mn create-app example.micronaut.micronautguide \
--features=junit-params,validation \
--build=gradle \
--lang=kotlin \
--test=junit
Table of Contents
Custom constraint annotation for validation
How to create a custom constraint annotation for validation in your Micronaut application
Authors: Sergio del Amo
Micronaut Version: 4.6.3
1. Getting Started
In this guide, we will create a Micronaut application written in Kotlin.
2. What you will need
To complete this guide, you will need the following:
-
Some time on your hands
-
A decent text editor or IDE (e.g. IntelliJ IDEA)
-
JDK 21 or greater installed with
JAVA_HOME
configured appropriately
3. Solution
We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.
-
Download and unzip the source
4. Writing the Application
Create an application using the Micronaut Command Line Interface or with Micronaut Launch.
If you don’t specify the --build argument, Gradle with the Kotlin DSL is used as the build tool. If you don’t specify the --lang argument, Java is used as the language.If you don’t specify the --test argument, JUnit is used for Java and Kotlin, and Spock is used for Groovy.
|
The previous command creates a Micronaut application with the default package example.micronaut
in a directory named micronautguide
.
If you use Micronaut Launch, select Micronaut Application as application type and add junit-params
, and validation
features.
If you have an existing Micronaut application and want to add the functionality described here, you can view the dependency and configuration changes from the specified features, and apply those changes to your application. |
5. Writing the application
In this guide, you will code an annotation - @E164
- to validate phone numbers.
E.164 is the international telephone numbering plan that ensures each device on the PSTN has globally unique number.
This number allows phone calls and text messages can be correctly routed to individual phones in different countries. E.164 numbers are formatted [+] [country code] [subscriber number including area code] and can have a maximum of fifteen digits.
5.1. Country Code
Create an enum for the country code. The following enum maps the ITU_T recommendation for E.164 assigned country codes.
package example.micronaut
/**
* Every country code in the world.
* @see <a href="https://www.itu.int/dms_pub/itu-t/opb/sp/T-SP-E.164D-11-2011-PDF-E.pdf">LIST OF ITU-T RECOMMENDATION E.164 ASSIGNED COUNTRY CODES</a>
*/
enum class CountryCode(val code: String, val countryName: String) {
AFGHANISTAN("93", "Afghanistan"),
ALBANIA("355", "Albania (Republic of)"),
ALGERIA("213", "Algeria (People's Democratic Republic of)"),
AMERICAN_SAMOA("1", "American Samoa"),
ANDORRA("376", "Andorra (Principality of)"),
ANGOLA("244", "Angola (Republic of)"),
ANGUILLA("1", "Anguilla"),
ANTIGUA_AND_BARBUDA("1", "Antigua and Barbuda"),
ARGENTINA("54", "Argentine Republic"),
ARMENIA("374", "Armenia (Republic of)"),
ARUBA("297", "Aruba"),
AUSTRALIA("61", "Australia"),
AUSTRALIAN_EXTERNAL_TERRITORIES("672", "Australian External Territories"),
AUSTRIA("43", "Austria"),
AZERBAIJAN("994", "Azerbaijan (Republic of)"),
BAHAMAS("1", "Bahamas (Commonwealth of the)"),
BAHRAIN("973", "Bahrain (Kingdom of)"),
BANGLADESH("880", "Bangladesh (People's Republic of)"),
BARBADOS("1", "Barbados"),
BELARUS("375", "Belarus (Republic of)"),
BELGIUM("32", "Belgium"),
BELIZE("501", "Belize"),
BENIN("229", "Benin (Republic of)"),
BERMUDA("1", "Bermuda"),
BHUTAN("975", "Bhutan (Kingdom of)"),
BOLIVIA("591", "Bolivia (Plurinational State of)"),
BONAIRE_SINT_EUSTATIUS_AND_SABA("599", "Bonaire, Sint Eustatius and Saba"),
BOSNIA_AND_HERZEGOVINA("387", "Bosnia and Herzegovina"),
BOTSWANA("267", "Botswana (Republic of)"),
BRAZIL("55", "Brazil (Federative Republic of)"),
BRITISH_VIRGIN_ISLANDS("1", "British Version Islands"),
BRUNEI("673", "Brunei Darussalam"),
BULGARIA("359", "Bulgaria (Republic of)"),
BURKINA_FASO("226", "Burkina Faso"),
BURUNDI("257", "Burundi (Republic of)"),
CABO_VERDE("238", "Cabo Verde (Republic of)"),
CAMBODIA("855", "Cambodia (Kingdom of"),
CAMEROON("237", "Cameroon (Republic of)"),
CANADA("1", "Canada"),
CAYMAN_ISLANDS("1", "Cayman Islands"),
CENTRAL_AFRICAN_REPUBLIC("236", "Central African Republic"),
CHAD("235", "Chad (Republic of)"),
CHILE("56", "Chile"),
CHINA("86", "China (People's Republic of)"),
COLOMBIA("57", "Colombia (Republic of)"),
COMOROS("269", "Comoros (Union of the)"),
CONGO("242", "Congo (Republic of the)"),
COOK_ISLANDS("682", "Cook Islands"),
COSTA_RICA("506", "Costa Rica"),
CROATIA("385", "Croatia (Republic of)"),
CUBA("53", "Cuba"),
CURACAO("599", "Curacao"),
CYPRUS("357", "Cyprus (Republic of)"),
CZECH_REPUBLIC("420", "Czech Republic"),
DEMOCRATIC_REPUBLIC_OF_THE_CONGO("243", "Democratic Republic of the Congo"),
DENMARK("45", "Denmark"),
DISASTER_RELIEF("888", "Telecommunications for Disaster Relief (TDR)"),
DJIBOUTI("253", "Djibouti (Republic of)"),
DOMINICA("1", "Dominica (Commonwealth of)"),
DOMINICAN_REPUBLIC("1", "Dominican Republic"),
EAST_TIMOR("670", "Timor-Leste (Democratic Republic of)"),
ECUADOR("593", "Ecuador"),
EGYPT("20", "Egypt (Arab Republic of)"),
EL_SALVADOR("503", "El Salvador (Republic of)"),
EQUATORIAL_GUINEA("240", "Equatorial Guinea (Republic of)"),
ERITREA("291", "Eritrea"),
ESTONIA("372", "Estonia (Republic of)"),
ETHIOPIA("251", "Ethiopia (Federal Democratic Republic of)"),
FALKLAND_ISLANDS("500", "Falkland Islands (Malvinas)"),
FAROE_ISLANDS("298", "Faroe Islands"),
FIJI("679", "Fiji (Republic of"),
FINLAND("358", "Finland"),
FRANCE("33", "France"),
FRENCH_GUIANA("590", "French Guiana (French Department of)"),
FRENCH_POLYNESIA("689", "French Polynesia"),
GABON("241", "Gabonese Republic"),
GAMBIA("220", "Gambia (Republic of)"),
GEORGIA("995", "Georgia"),
GERMANY("49", "Germany (Federal Republic of)"),
GHANA("233", "Ghana"),
GIBRALTAR("350", "Gibraltar"),
GMSS("881", "Global Missile Satellite System (GMSS), shared code"),
GREECE("30", "Greece"),
GREENLAND("299", "Greenland (Denmark)"),
GRENADA("1", "Grenada"),
GROUP_SHARED("388", "Group of countries, shared code"),
GUADELOUPE("590", "Guadeloupe (French Department of"),
GUAM("1", "Guam"),
GUATEMALA("502", "Guatemala (Republic of)"),
GUINEA("224", "Guinea (Republic of)"),
GUINEA_BISSAU("245", "Guinnea-Bassau (Republic of)"),
GUYANA("592", "Guyana"),
HAITI("509", "Haiti (Republic of)"),
HONDURAS("504", "Honduras (Republic of)"),
HONG_KONG("852", "Hong Kong, China"),
HUNGARY("36", "Hungary (Republic of)"),
ICELAND("354", "Iceland"),
INDIA("91", "India (Republic of)"),
INDONESIA("62", "Indonesia"),
INMARSAT("870", "Inmarsat SNAC"),
INTERNATIONAL_FREEPHONE("800", "International Freephone Service"),
INTERNATIONAL_NETWORKS("882", "International Networks, shared code"), //There is a second entry for 883 with the same name.
INTERNATIONAL_PREMIUM("979", "International Premium Rate Service (IPRS)"),
INTERNATIONAL_SHARED("808", "International Shared Cost Service (ISCS)"),
INTERNATIONAL_TRIAL("991", "Trial of a proposed new international telecommunication public correspondence service, shared code"),
IRAN("98", "Iran (Islamic Republic of)"),
IRAQ("964", "Iraq (Republic of)"),
IRELAND("353", "Ireland"),
ISRAEL("972", "Israel (State of)"),
ITALY("39", "Italy"),
IVORY_COAST("225", "Cote d'Ivoire (Republic of)"),
JAMAICA("1", "Jamaica"),
JAPAN("81", "Japan"),
JORDAN("962", "Jordan (Hashemite Kingdom of)"),
KAZAKHSTAN("7", "Kazakhstan (Republic of)"),
KENYA("254", "Kenya (Republic of)"),
KIRIBATI("686", "Kiribati (Republic of)"),
KOSOVO("383", "Kosovo"),
KUWAIT("965", "Kuwait (State of)"),
KYRGYZSTAN("996", "Kyrgyz Republic"),
LAOS("856", "Lao People's Democratic Republic"),
LATVIA("371", "Latvia (Republic of)"),
LEBANON("961", "Lebanon"),
LESOTHO("266", "Lesotho (Kingdom of)"),
LIBERIA("231", "Liberia (Republic of)"),
LIBYA("218", "Libya"),
LIECHTENSTEIN("423", "Liechtenstein (Principality of)"),
LITHUANIA("370", "Lithuania (Republic of)"),
LUXEMBOURG("352", "Luxembourg"),
MACAO("853", "Macao, China"),
MACEDONIA("389", "The Former Yugoslav Republic of Macedonia"),
MADAGASCAR("261", "Madagascar (Republic of)"),
MALAWI("265", "Malawi"),
MALAYSIA("60", "Malaysia"),
MALDIVES("960", "Maldives (Republic of)"),
MALI("223", "Mali (Republic of)"),
MALTA("356", "Malta"),
MARSHALL_ISLANDS("692", "Marshall Islands (Republic of)"),
MARTINIQUE("596", "Martinique (French Department of"),
MAURITANIA("222", "Mauritania (Islamic Republic of)"),
MAURITIUS("230", "Mauritius (Republic of)"),
MEXICO("52", "Mexico"),
MICRONESIA("691", "Micronesia (Federated States of)"),
MOLDOVA("373", "Moldova (Republic of)"),
MONACO("377", "Monaco (Principality of)"),
MONGOLIA("976", "Mongolia"),
MONTENEGRO("382", "Montenegro (Republic of)"),
MONTSERRAT("1", "Montserrat"),
MOROCCO("212", "Morocco (Kingdom of)"),
MOZAMBIQUE("258", "Mozambique (Republic of)"),
MYANMAR("95", "Myanmar (The Republic of the Union of)"),
NAMIBIA("264", "Namibia (Republic of)"),
NAURU("674", "Nauru (Republic of)"),
NEPAL("977", "Nepal (Federal Democratic Republic of)"),
NETHERLANDS("31", "Netherlands (Kingdom of the)"),
NEW_CALEDONIA("687", "New Caledonia (Territoire francais d'outre-mer)"),
NEW_ZEALAND("64", "New Zealand"),
NICARAGUA("505", "Nicaragua"),
NIGER("227", "Niger (Republic of)"),
NIGERIA("234", "Nigeria (Federal Republic of)"),
NIUE("683", "Niue"),
NORTH_KOREA("850", "Democratic People's Republic of Korea\n"),
NORTHERN_MARIANA_ISLANDS("1", "Northern Mariana Islands (Commonwealth of the)"),
NORWAY("47", "Norway"),
OMAN("968", "Oman (Sultanate of)"),
PAKISTAN("92", "Pakistan (Islamic Republic of)"),
PALAU("680", "Palau (Republic of)"),
PANAMA("507", "Panama (Republic of)"),
PAPUA_NEW_GUINEA("675", "Papua New Guinea"),
PARAGUAY("595", "PARAGUAY (Republic of)"),
PERU("51", "Peru"),
PHILIPPINES("63", "Philippines (Republic of the)"),
POLAND("48", "Poland (Republic of)"),
PORTUGAL("351", "Portugal"),
PUERTO_RICO("1", "Puerto Rico"),
QATAR("974", "Qatar (State of)"),
REUNION("262", "French Departments and Territories in the Indian Ocean"),
ROMANIA("40", "Romania"),
RUSSIA("7", "Russian Federation"),
RWANDA("250", "Rwanda (Republic of)"),
SAINT_HELENA("290", "Saint Helena, Ascension and the Tristan da Cunha"), //Also, 247 has an identical entry
SAINT_KITTS_AND_NEVIS("1", "Saint Kitts and Nevis"),
SAINT_LUCIA("1", "Saint Lucia"),
SAINT_PIERRE_AND_MIQUELON("508", "Saint Pierre and Miquelon (Collectivite territoriale de la Republique francaise)"),
SAINT_VINCENT_AND_THE_GRENADINES("1", "Saint Vincent and the Grenadines"),
SAMOA("685", "Samoa (Independent State of"),
SAN_MARINO("378", "San Marino (Republic of)"),
SAO_TOME_AND_PRINCIPE("239", "Sao Tome and Principe (Democratic Republic of)"),
SAUDI_ARABIA("966", "Saudi Arabia (Kingdom of)"),
SENEGAL("221", "Senegal (Republic of)"),
SERBIA("381", "Serbia (Republic of)"),
SEYCHELLES("248", "Seychelles (Republic of)"),
SIERRA_LEONE("232", "Sierra Leone"),
SINGAPORE("65", "Singapore (Republic of)"),
SINT_MAARTEN("1", "Sint Maarten (Dutch part)"),
SLOVAKIA("421", "Slovak Republic"),
SLOVENIA("386", "Slovenia (Republic of)"),
SOLOMON_ISLANDS("677", "Solomon Islands"),
SOMALIA("252", "Somalia (Federal Republic of)"),
SOUTH_AFRICA("27", "South Africa (Republic of)"),
SOUTH_KOREA("82", "Korea (Republic of)"),
SOUTH_SUDAN("211", "South Sudan (Republic of)"),
SPAIN("34", "Spain"),
SRI_LANKA("94", "Sri Lanka (Democratic Socialist Republic of)"),
SUDAN("249", "Sudan (Republic of)"),
SURINAME("597", "Suriname (Republic of"),
SWAZILAND("268", "Swaziland (Kingdom of)"),
SWEDEN("46", "Sweden"),
SWITZERLAND("41", "Switzerland (Confederation of)"),
SYRIA("963", "Syrian Arab Republic"),
TAIWAN("886", "Taiwan, China"), //Republic of China?
TAJIKISTAN("992", "Tajikstan (Republic of)"),
TANZANIA("255", "Tanzania (United Republic of)"),
THAILAND("66", "Thailand"),
TOGO("228", "Togolese Republic"),
TOKELAU("690", "Tokelau"),
TONGA("676", "Tonga (Kingdom of)"),
TRINIDAD_AND_TOBAGO("1", "Trinidad and Tobago"),
TUNISIA("216", "Tunisia"),
TURKEY("90", "Turkey"),
TURKMENISTAN("993", "Turkemenistan"),
TURKS_AND_CAICOS_ISLANDS("1", "Turks and Caicos Islands"),
TUVALU("688", "Tuvalu"),
UGANDA("256", "Uganda (Republic of)"),
UKRAINE("380", "Ukraine"),
UNITED_ARAB_EMIRATES("971", "United Arab Emirates"),
UNITED_KINGDOM("44", "United Kingdom of Great Britain and Northern Ireland"),
UNITED_STATES("1", "United States of America"),
UPT("878", "Universal Personal Telecommunication Service (UPT)"),
URUGUAY("598", "Uruguay (Eastern Republic of)"),
UZBEKISTAN("998", "Uzbekistan (Republic of)"),
VANUATU("678", "Vanuatu (Republic of)"),
VATICAN("379", "Vatican City State"),
VENEZUELA("58", "Venezuala (Bolivarian Republic of)"),
VIETNAM("84", "Viet nam (Socialist Republic of)"),
WALLIS_AND_FUTUNA("681", "Wallis and Futuna (Territoire francais d'outre-mer)"),
YEMEN("967", "Yemen (Republic of)"),
ZAMBIA("260", "Zambia (Republic of)"),
ZIMBABWE("263", "Zimbabwe (Republic of)");
override fun toString(): String {
return code
}
companion object {
private const val PLUS_SIGN = "+"
val COUNTRYCODESBYCODE = CountryCode.values().groupBy { it.code }
val CODES = COUNTRYCODESBYCODE.keys
.sortedByDescending { it.length }
.toList()
fun parseCountryCode(number: String): String? {
val phone = if (number.startsWith(PLUS_SIGN)) number.substring(1) else number
return CODES.find { phone.startsWith(it) }
}
}
}
And a test for this enum:
package example.micronaut
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class CountryCodeTest {
@Test
fun preferredNameGetsUsed() {
val name = CountryCode.YEMEN.countryName
assertEquals(name, "Yemen (Republic of)")
}
@Test
fun defaultNameIsCapitalizedCorrectly() {
val name = CountryCode.SPAIN.countryName
assertEquals(name, "Spain")
}
@Test
fun toStringReturnsCorrectValue() {
val code = CountryCode.AMERICAN_SAMOA.toString()
assertEquals(code, "1")
}
@Test
fun countryCodeGetCodesReturnEveryCodeWithLongestCodesFirst() {
assertTrue(CountryCode.CODES[0].length > 1)
}
@Test
fun countryCodeParseCountryCodeParseCodes() {
Assertions.assertNull(CountryCode.parseCountryCode("999999"))
assertEquals("34", CountryCode.parseCountryCode("34630443322"))
assertEquals("268", CountryCode.parseCountryCode("2684046441"))
assertEquals("1", CountryCode.parseCountryCode("+14155552671"))
}
@Test
fun countryCodeCountryCodesByCodeReturnAListOfCountryCodeWithTheSameCountryCode() {
assertTrue(CountryCode.COUNTRYCODESBYCODE["999999"] == null)
assertEquals(
CountryCode.COUNTRYCODESBYCODE["1"], listOf(
CountryCode.AMERICAN_SAMOA,
CountryCode.ANGUILLA,
CountryCode.ANTIGUA_AND_BARBUDA,
CountryCode.BAHAMAS,
CountryCode.BARBADOS,
CountryCode.BERMUDA,
CountryCode.BRITISH_VIRGIN_ISLANDS,
CountryCode.CANADA,
CountryCode.CAYMAN_ISLANDS,
CountryCode.DOMINICA,
CountryCode.DOMINICAN_REPUBLIC,
CountryCode.GRENADA,
CountryCode.GUAM,
CountryCode.JAMAICA,
CountryCode.MONTSERRAT,
CountryCode.NORTHERN_MARIANA_ISLANDS,
CountryCode.PUERTO_RICO,
CountryCode.SAINT_KITTS_AND_NEVIS,
CountryCode.SAINT_LUCIA,
CountryCode.SAINT_VINCENT_AND_THE_GRENADINES,
CountryCode.SINT_MAARTEN,
CountryCode.TRINIDAD_AND_TOBAGO,
CountryCode.TURKS_AND_CAICOS_ISLANDS,
CountryCode.UNITED_STATES
)
)
}
}
6. Phone E164
Create a Utils
class to validate a phone number.
package example.micronaut
import io.micronaut.core.util.StringUtils
object E164Utils {
private const val MAX_NUMBER_OF_DIGITS = 15
private const val PLUS_SIGN = "+"
fun isValid(value: String?): Boolean {
if (value.isNullOrEmpty()) {
return false
}
val phone = if (value.startsWith(PLUS_SIGN)) value.substring(1) else value
if (phone.length > MAX_NUMBER_OF_DIGITS) {
return false
}
if (phone.isEmpty()) {
return false
}
if (!StringUtils.isDigits(phone) || phone[0] == '0') {
return false
}
return CountryCode.parseCountryCode(phone) != null
}
}
and a test for valid and invalid phones:
package example.micronaut
import example.micronaut.E164Utils.isValid
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
class E164UtilsTest {
@ParameterizedTest
@ValueSource(
strings = [
"+04630443322",
"+1415555267102345",
"+1-4155552671",
""]
)
fun invalidPhones(phone: String?) {
assertFalse(isValid(phone))
}
@ParameterizedTest
@ValueSource(
strings = [
"+14155552671",
"+442071838750",
"+55115525632",
"14155552671",
"442071838750",
"55115525632",
"55115525632"]
)
fun validPhones(phone: String?) {
assertTrue(isValid(phone))
}
}
7. Custom Validation Annotation
Create a custom annotation:
package example.micronaut
import jakarta.validation.Constraint
/**
* The annotated element must be a E.164 phone number.
*
* @see <a href="https://www.itu.int/rec/T-REC-E.164/en">ITU E.164 recommendation</a>
* @see <a href="https://www.twilio.com/docs/glossary/what-e164">E.614</a>
*/
@Target(
AnnotationTarget.FIELD,
AnnotationTarget.FUNCTION,
AnnotationTarget.ANNOTATION_CLASS,
AnnotationTarget.VALUE_PARAMETER,
AnnotationTarget.TYPE
)
@Retention(AnnotationRetention.RUNTIME)
@Repeatable
@MustBeDocumented
@Constraint(validatedBy = [])
annotation class E164
8. Validation Factory
Create a factory that creates a ConstraintValidator
for the annotation defined in the previous step.
package example.micronaut
import example.micronaut.E164Utils.isValid
import io.micronaut.context.annotation.Factory
import io.micronaut.validation.validator.constraints.ConstraintValidator
import jakarta.inject.Singleton
@Factory (1)
class CustomValidationFactory {
@Singleton (2)
fun e164Validator(): ConstraintValidator<E164, String> {
return ConstraintValidator { value, _, _ -> isValid(value) }
}
}
1 | A class annotated with the @Factory annotated is a factory. It provides one or more methods annotated with a bean scope annotation (e.g. @Singleton ). Read more about Bean factories. |
2 | Use jakarta.inject.Singleton to designate a class as a singleton. |
9. Validation Messages
Create a default message for the E164 constraint:
package example.micronaut
import io.micronaut.context.StaticMessageSource
import jakarta.inject.Singleton
@Singleton
class CustomValidationMessages : StaticMessageSource() {
init {
addMessage(E164::class.java.getName() + MESSAGE_SUFFIX, E164_MESSAGE)
}
companion object {
private const val E164_MESSAGE = "must be a phone in E.164 format"
private const val MESSAGE_SUFFIX = ".message"
}
}
10. Testing Validation
Create a Contact
object which uses the custom annotation.
package example.micronaut
import io.micronaut.core.annotation.Introspected
import io.micronaut.core.annotation.NonNull
import jakarta.validation.constraints.NotBlank
@Introspected
class Contact(
@field:E164
@field:NotBlank
@field:NonNull
val phone: String
)
Create a test verifies the custom annotation participates in the validation of the object.
package example.micronaut
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import jakarta.validation.Validator
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
@MicronautTest(startApplication = false) (1)
class ContactTest(private val validator: Validator) { (2)
@Test
fun contactValidation() {
assertTrue(validator.validate(Contact("+14155552671")).isEmpty())
val violationSet = validator.validate(Contact("+1-4155552671"))
assertFalse(violationSet.isEmpty())
val template = "{example.micronaut.E164.message}"
assertTrue(
violationSet
.any {
it.messageTemplate == template &&
it.invalidValue == "+1-4155552671" &&
it.message == "must be a phone in E.164 format"
})
}
}
1 | Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context. This test does not need the embedded server. Set startApplication to false to avoid starting it. |
2 | Injection for Validator . |
11. Next Steps
Explore more features with Micronaut Guides.
Learn about Bean Validation and how to localize your application.
12. Help with the Micronaut Framework
The Micronaut Foundation sponsored the creation of this Guide. A variety of consulting and support services are available.
13. License
All guides are released with an Apache license 2.0 license for the code and a Creative Commons Attribution 4.0 license for the writing and media (images…). |