You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by kw...@apache.org on 2014/11/28 15:50:39 UTC
svn commit: r1642306 - in /sling/trunk/contrib/extensions/validation:
api/src/main/java/org/apache/sling/validation/api/
core/src/main/java/org/apache/sling/validation/impl/
core/src/main/java/org/apache/sling/validation/impl/util/
core/src/test/java/o...
Author: kwin
Date: Fri Nov 28 14:50:39 2014
New Revision: 1642306
URL: http://svn.apache.org/r1642306
Log:
SLING-4013 allow optional properties/child resources within validation model
Modified:
sling/trunk/contrib/extensions/validation/api/src/main/java/org/apache/sling/validation/api/ChildResource.java
sling/trunk/contrib/extensions/validation/api/src/main/java/org/apache/sling/validation/api/ResourceProperty.java
sling/trunk/contrib/extensions/validation/core/src/main/java/org/apache/sling/validation/impl/ChildResourceImpl.java
sling/trunk/contrib/extensions/validation/core/src/main/java/org/apache/sling/validation/impl/Constants.java
sling/trunk/contrib/extensions/validation/core/src/main/java/org/apache/sling/validation/impl/ResourcePropertyImpl.java
sling/trunk/contrib/extensions/validation/core/src/main/java/org/apache/sling/validation/impl/ValidationServiceImpl.java
sling/trunk/contrib/extensions/validation/core/src/main/java/org/apache/sling/validation/impl/util/JCRBuilder.java
sling/trunk/contrib/extensions/validation/core/src/test/java/org/apache/sling/validation/impl/ValidationServiceImplTest.java
Modified: sling/trunk/contrib/extensions/validation/api/src/main/java/org/apache/sling/validation/api/ChildResource.java
URL: http://svn.apache.org/viewvc/sling/trunk/contrib/extensions/validation/api/src/main/java/org/apache/sling/validation/api/ChildResource.java?rev=1642306&r1=1642305&r2=1642306&view=diff
==============================================================================
--- sling/trunk/contrib/extensions/validation/api/src/main/java/org/apache/sling/validation/api/ChildResource.java (original)
+++ sling/trunk/contrib/extensions/validation/api/src/main/java/org/apache/sling/validation/api/ChildResource.java Fri Nov 28 14:50:39 2014
@@ -44,6 +44,13 @@ public interface ChildResource {
Pattern getNamePattern();
/**
+ * Returns {@code true} if at least one resource matching the name/namePattern is required.
+ *
+ * @return {@code true} if the resource is required, {@code false} otherwise
+ */
+ boolean isRequired();
+
+ /**
* Returns the properties this child resource is expected to have.
*
* @return the properties set
Modified: sling/trunk/contrib/extensions/validation/api/src/main/java/org/apache/sling/validation/api/ResourceProperty.java
URL: http://svn.apache.org/viewvc/sling/trunk/contrib/extensions/validation/api/src/main/java/org/apache/sling/validation/api/ResourceProperty.java?rev=1642306&r1=1642305&r2=1642306&view=diff
==============================================================================
--- sling/trunk/contrib/extensions/validation/api/src/main/java/org/apache/sling/validation/api/ResourceProperty.java (original)
+++ sling/trunk/contrib/extensions/validation/api/src/main/java/org/apache/sling/validation/api/ResourceProperty.java Fri Nov 28 14:50:39 2014
@@ -49,6 +49,13 @@ public interface ResourceProperty {
boolean isMultiple();
/**
+ * Returns {@code true} if at least one property matching the name/namePattern is required.
+ *
+ * @return {@code true} if the property is required, {@code false} otherwise
+ */
+ boolean isRequired();
+
+ /**
* Returns a list of {@link ParameterizedValidator}s which should be applied on this property.
*
* @return the list of validators
Modified: sling/trunk/contrib/extensions/validation/core/src/main/java/org/apache/sling/validation/impl/ChildResourceImpl.java
URL: http://svn.apache.org/viewvc/sling/trunk/contrib/extensions/validation/core/src/main/java/org/apache/sling/validation/impl/ChildResourceImpl.java?rev=1642306&r1=1642305&r2=1642306&view=diff
==============================================================================
--- sling/trunk/contrib/extensions/validation/core/src/main/java/org/apache/sling/validation/impl/ChildResourceImpl.java (original)
+++ sling/trunk/contrib/extensions/validation/core/src/main/java/org/apache/sling/validation/impl/ChildResourceImpl.java Fri Nov 28 14:50:39 2014
@@ -8,6 +8,7 @@ import java.util.regex.PatternSyntaxExce
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.commons.osgi.PropertiesUtil;
import org.apache.sling.validation.api.ChildResource;
import org.apache.sling.validation.api.ResourceProperty;
import org.apache.sling.validation.api.Validator;
@@ -22,6 +23,7 @@ public class ChildResourceImpl implement
private final Pattern namePattern;
private final Set<ResourceProperty> properties;
private final List<ChildResource> children;
+ private final boolean isRequired;
public ChildResourceImpl(Resource modelResource, Resource childResource, Map<String, Validator<?>> validatorsMap, List<ChildResource> children) {
String root = modelResource.getPath();
@@ -46,7 +48,7 @@ public class ChildResourceImpl implement
name = childResource.getName();
namePattern = null;
}
-
+ isRequired = !PropertiesUtil.toBoolean(childrenProperties.get(Constants.OPTIONAL), false);
properties = JCRBuilder.buildProperties(validatorsMap, childResource.getChild(Constants.PROPERTIES));
this.children = children;
}
@@ -69,4 +71,9 @@ public class ChildResourceImpl implement
public List<ChildResource> getChildren() {
return children;
}
+
+ @Override
+ public boolean isRequired() {
+ return isRequired;
+ }
}
Modified: sling/trunk/contrib/extensions/validation/core/src/main/java/org/apache/sling/validation/impl/Constants.java
URL: http://svn.apache.org/viewvc/sling/trunk/contrib/extensions/validation/core/src/main/java/org/apache/sling/validation/impl/Constants.java?rev=1642306&r1=1642305&r2=1642306&view=diff
==============================================================================
--- sling/trunk/contrib/extensions/validation/core/src/main/java/org/apache/sling/validation/impl/Constants.java (original)
+++ sling/trunk/contrib/extensions/validation/core/src/main/java/org/apache/sling/validation/impl/Constants.java Fri Nov 28 14:50:39 2014
@@ -30,6 +30,7 @@ public final class Constants {
public static final String PROPERTIES = "properties";
public static final String PROPERTY_TYPE = "propertyType";
public static final String PROPERTY_MULTIPLE = "propertyMultiple";
+ public static final String OPTIONAL = "optional";
public static final String VALIDATORS = "validators";
public static final String VALIDATOR_ARGUMENTS = "validatorArguments";
public static final String CHILDREN = "children";
Modified: sling/trunk/contrib/extensions/validation/core/src/main/java/org/apache/sling/validation/impl/ResourcePropertyImpl.java
URL: http://svn.apache.org/viewvc/sling/trunk/contrib/extensions/validation/core/src/main/java/org/apache/sling/validation/impl/ResourcePropertyImpl.java?rev=1642306&r1=1642305&r2=1642306&view=diff
==============================================================================
--- sling/trunk/contrib/extensions/validation/core/src/main/java/org/apache/sling/validation/impl/ResourcePropertyImpl.java (original)
+++ sling/trunk/contrib/extensions/validation/core/src/main/java/org/apache/sling/validation/impl/ResourcePropertyImpl.java Fri Nov 28 14:50:39 2014
@@ -28,10 +28,11 @@ public class ResourcePropertyImpl implem
private String name;
private boolean isMultiple;
+ private boolean isRequired;
private List<ParameterizedValidator> validators;
private Pattern namePattern;
- public ResourcePropertyImpl(String name, String nameRegex, boolean isMultiple, List<ParameterizedValidator> validators) {
+ public ResourcePropertyImpl(String name, String nameRegex, boolean isMultiple, boolean isRequired, List<ParameterizedValidator> validators) {
if (nameRegex != null) {
this.name = null;
this.namePattern = Pattern.compile(nameRegex);
@@ -40,6 +41,7 @@ public class ResourcePropertyImpl implem
this.namePattern = null;
}
this.isMultiple = isMultiple;
+ this.isRequired = isRequired;
this.validators = validators;
}
@@ -59,6 +61,11 @@ public class ResourcePropertyImpl implem
}
@Override
+ public boolean isRequired() {
+ return isRequired;
+ }
+
+ @Override
public List<ParameterizedValidator> getValidators() {
return validators;
}
Modified: sling/trunk/contrib/extensions/validation/core/src/main/java/org/apache/sling/validation/impl/ValidationServiceImpl.java
URL: http://svn.apache.org/viewvc/sling/trunk/contrib/extensions/validation/core/src/main/java/org/apache/sling/validation/impl/ValidationServiceImpl.java?rev=1642306&r1=1642305&r2=1642306&view=diff
==============================================================================
--- sling/trunk/contrib/extensions/validation/core/src/main/java/org/apache/sling/validation/impl/ValidationServiceImpl.java (original)
+++ sling/trunk/contrib/extensions/validation/core/src/main/java/org/apache/sling/validation/impl/ValidationServiceImpl.java Fri Nov 28 14:50:39 2014
@@ -147,14 +147,14 @@ public class ValidationServiceImpl imple
foundMatch = true;
}
}
- if (!foundMatch) {
+ if (!foundMatch && childResource.isRequired()) {
result.addFailureMessage(relativePath + childResource.getNamePattern().pattern(), "Missing required child resource.");
}
} else {
Resource expectedResource = resource.getChild(childResource.getName());
if (expectedResource != null) {
validateChildResource(expectedResource, relativePath, childResource, result);
- } else {
+ } else if (childResource.isRequired()) {
result.addFailureMessage(relativePath + childResource.getName(), "Missing required child resource.");
}
}
@@ -249,7 +249,7 @@ public class ValidationServiceImpl imple
validateValueMap(key, valueMap, relativePath, resourceProperty, result);
}
}
- if (!foundMatch) {
+ if (!foundMatch && resourceProperty.isRequired()) {
result.addFailureMessage(relativePath + resourceProperty.getNamePattern(), "Missing required property.");
}
} else {
@@ -262,7 +262,9 @@ public class ValidationServiceImpl imple
private void validateValueMap(String property, ValueMap valueMap, String relativePath, ResourceProperty resourceProperty, ValidationResultImpl result) {
Object fieldValues = valueMap.get(property);
if (fieldValues == null) {
- result.addFailureMessage(relativePath + property, "Missing required property.");
+ if (resourceProperty.isRequired()) {
+ result.addFailureMessage(relativePath + property, "Missing required property.");
+ }
return;
}
List<ParameterizedValidator> validators = resourceProperty.getValidators();
Modified: sling/trunk/contrib/extensions/validation/core/src/main/java/org/apache/sling/validation/impl/util/JCRBuilder.java
URL: http://svn.apache.org/viewvc/sling/trunk/contrib/extensions/validation/core/src/main/java/org/apache/sling/validation/impl/util/JCRBuilder.java?rev=1642306&r1=1642305&r2=1642306&view=diff
==============================================================================
--- sling/trunk/contrib/extensions/validation/core/src/main/java/org/apache/sling/validation/impl/util/JCRBuilder.java (original)
+++ sling/trunk/contrib/extensions/validation/core/src/main/java/org/apache/sling/validation/impl/util/JCRBuilder.java Fri Nov 28 14:50:39 2014
@@ -59,6 +59,7 @@ public class JCRBuilder {
String fieldName = property.getName();
ValueMap propertyValueMap = property.adaptTo(ValueMap.class);
Boolean propertyMultiple = PropertiesUtil.toBoolean(propertyValueMap.get(Constants.PROPERTY_MULTIPLE), false);
+ Boolean propertyRequired = !PropertiesUtil.toBoolean(propertyValueMap.get(Constants.OPTIONAL), false);
String nameRegex = PropertiesUtil.toString(propertyValueMap.get(Constants.NAME_REGEX), null);
Resource validators = property.getChild(Constants.VALIDATORS);
List<ParameterizedValidator> parameterizedValidators = new ArrayList<ParameterizedValidator>();
@@ -87,7 +88,7 @@ public class JCRBuilder {
parameterizedValidators.add(new ParameterizedValidatorImpl(v, new ValueMapDecorator(validatorArgumentsMap)));
}
}
- ResourceProperty f = new ResourcePropertyImpl(fieldName, nameRegex, propertyMultiple, parameterizedValidators);
+ ResourceProperty f = new ResourcePropertyImpl(fieldName, nameRegex, propertyMultiple, propertyRequired, parameterizedValidators);
properties.add(f);
}
}
Modified: sling/trunk/contrib/extensions/validation/core/src/test/java/org/apache/sling/validation/impl/ValidationServiceImplTest.java
URL: http://svn.apache.org/viewvc/sling/trunk/contrib/extensions/validation/core/src/test/java/org/apache/sling/validation/impl/ValidationServiceImplTest.java?rev=1642306&r1=1642305&r2=1642306&view=diff
==============================================================================
--- sling/trunk/contrib/extensions/validation/core/src/test/java/org/apache/sling/validation/impl/ValidationServiceImplTest.java (original)
+++ sling/trunk/contrib/extensions/validation/core/src/test/java/org/apache/sling/validation/impl/ValidationServiceImplTest.java Fri Nov 28 14:50:39 2014
@@ -226,6 +226,76 @@ public class ValidationServiceImplTest {
}
}
}
+
+ @Test()
+ public void testValueMapWithMissingOptionalValue() throws Exception {
+ validationService.validators.put("org.apache.sling.validation.impl.validators.RegexValidator", new RegexValidator());
+
+ TestProperty property = new TestProperty("field1");
+ property.optional = true;
+ property.addValidator("org.apache.sling.validation.impl.validators.RegexValidator", "");
+
+ ResourceResolver rr = rrf.getAdministrativeResourceResolver(null);
+ Resource model1 = null;
+ try {
+ if (rr != null) {
+ model1 = createValidationModelResource(rr, libsValidatorsRoot.getPath(), "testValidationModel1", "sling/validation/test",
+ new String[]{"/apps/validation"}, property);
+ }
+ ValidationModel vm = validationService.getValidationModel("sling/validation/test", "/apps/validation/1/resource");
+ HashMap<String, Object> hashMap = new HashMap<String, Object>() {{
+ put("field2", "1");
+ }};
+ ValueMap map = new ValueMapDecorator(hashMap);
+ ValidationResult vr = validationService.validate(map, vm);
+ Assert.assertTrue(vr.isValid());
+ } finally {
+ if (model1 != null) {
+ rr.delete(model1);
+ }
+ if (rr != null) {
+ rr.commit();
+ rr.close();
+ }
+ }
+ }
+
+ @Test()
+ public void testValueMapWithEmptyOptionalValue() throws Exception {
+ validationService.validators.put("org.apache.sling.validation.impl.validators.RegexValidator", new RegexValidator());
+
+ TestProperty property = new TestProperty("field1");
+ property.optional = true;
+ property.addValidator("org.apache.sling.validation.impl.validators.RegexValidator", "regex=abc");
+
+ ResourceResolver rr = rrf.getAdministrativeResourceResolver(null);
+ Resource model1 = null;
+ try {
+ if (rr != null) {
+ model1 = createValidationModelResource(rr, libsValidatorsRoot.getPath(), "testValidationModel1", "sling/validation/test",
+ new String[]{"/apps/validation"}, property);
+ }
+ ValidationModel vm = validationService.getValidationModel("sling/validation/test", "/apps/validation/1/resource");
+ HashMap<String, Object> hashMap = new HashMap<String, Object>() {{
+ put("field1", "");
+ }};
+ ValueMap map = new ValueMapDecorator(hashMap);
+ ValidationResult vr = validationService.validate(map, vm);
+ Assert.assertFalse(vr.isValid());
+ // check for correct error message
+ Map<String, List<String>> expectedFailureMessages = new HashMap<String, List<String>>();
+ expectedFailureMessages.put("field2", Arrays.asList("Property does not match the pattern abc"));
+ Assert.assertThat(vr.getFailureMessages().entrySet(), Matchers.equalTo(expectedFailureMessages.entrySet()));
+ } finally {
+ if (model1 != null) {
+ rr.delete(model1);
+ }
+ if (rr != null) {
+ rr.commit();
+ rr.close();
+ }
+ }
+ }
@Test
public void testValueMapWithCorrectDataType() throws Exception {
@@ -281,27 +351,23 @@ public class ValidationServiceImplTest {
model1 = createValidationModelResource(rr, libsValidatorsRoot.getPath(), "testValidationModel1", "sling/validation/test",
new String[]{"/apps/validation"}, property);
- Resource child = createValidationModelChildResource(model1, "child1", null, new TestProperty("hello"));
- createValidationModelChildResource(child, "grandChild1", null, new TestProperty("hello"));
+ Resource child = createValidationModelChildResource(model1, "child1", null, false, new TestProperty("hello"));
+ createValidationModelChildResource(child, "grandChild1", null, false, new TestProperty("hello"));
testResource = ResourceUtil.getOrCreateResource(rr, "/apps/validation/1/resource", JcrConstants.NT_UNSTRUCTURED,
JcrConstants.NT_UNSTRUCTURED, true);
ModifiableValueMap mvm = testResource.adaptTo(ModifiableValueMap.class);
mvm.put("field1", "1");
- rr.commit();
Resource childResource = rr.create(testResource, "child1", new HashMap<String, Object>(){{
put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED);
}});
- rr.commit();
-
Resource resourceChild = rr.create(testResource, "child1", new HashMap<String, Object>(){{
put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED);
}});
mvm = resourceChild.adaptTo(ModifiableValueMap.class);
mvm.put("hello", "1");
- rr.commit();
// /apps/validation/1/resource/child1/grandChild1 will miss its mandatory "hello" property
Resource resourceGrandChild = rr.create(resourceChild, "grandChild1", new HashMap<String, Object>(){{
@@ -320,15 +386,54 @@ public class ValidationServiceImplTest {
assertThat(vr.getFailureMessages().keySet(), Matchers.hasSize(1));
} finally {
if (rr != null) {
- if (model1 != null) {
- rr.delete(model1);
- }
- if (testResource != null) {
- rr.delete(testResource);
- }
+ rr.delete(model1);
+ }
+ if (testResource != null) {
+ rr.delete(testResource);
+ }
+ rr.commit();
+ rr.close();
+ }
+ }
+
+ @Test
+ public void testResourceWithMissingOptionalChildProperty() throws Exception {
+ validationService.validators.put("org.apache.sling.validation.impl.validators.RegexValidator", new RegexValidator());
+
+ TestProperty property = new TestProperty("field1");
+ property.addValidator("org.apache.sling.validation.impl.validators.RegexValidator", RegexValidator.REGEX_PARAM + "=" + "\\d");
+ ResourceResolver rr = rrf.getAdministrativeResourceResolver(null);
+ Resource model1 = null;
+ Resource testResource = null;
+ try {
+ if (rr != null) {
+ model1 = createValidationModelResource(rr, libsValidatorsRoot.getPath(), "testValidationModel1", "sling/validation/test",
+ new String[]{"/apps/validation"}, property);
+
+ createValidationModelChildResource(model1, "child1", null, true, new TestProperty("hello"));
+
+ testResource = ResourceUtil.getOrCreateResource(rr, "/apps/validation/1/resource", JcrConstants.NT_UNSTRUCTURED,
+ JcrConstants.NT_UNSTRUCTURED, true);
+ ModifiableValueMap mvm = testResource.adaptTo(ModifiableValueMap.class);
+ mvm.put("field1", "1");
+
+ rr.create(testResource, "child2", new HashMap<String, Object>(){{
+ put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED);
+ }});
rr.commit();
- rr.close();
}
+ ValidationModel vm = validationService.getValidationModel("sling/validation/test", "/apps/validation/1/resource");
+ ValidationResult vr = validationService.validate(testResource, vm);
+ assertTrue(vr.isValid());
+ } finally {
+ if (rr != null) {
+ rr.delete(model1);
+ }
+ if (testResource != null) {
+ rr.delete(testResource);
+ }
+ rr.commit();
+ rr.close();
}
}
@@ -346,8 +451,8 @@ public class ValidationServiceImplTest {
model1 = createValidationModelResource(rr, libsValidatorsRoot.getPath(), "testValidationModel1", "sling/validation/test",
new String[]{"/apps/validation"}, property);
- Resource child = createValidationModelChildResource(model1, "child1", null, new TestProperty("hello"));
- createValidationModelChildResource(child, "grandChild1", null, new TestProperty("hello"));
+ Resource child = createValidationModelChildResource(model1, "child1", null, false, new TestProperty("hello"));
+ createValidationModelChildResource(child, "grandChild1", null, false, new TestProperty("hello"));
testResource = ResourceUtil.getOrCreateResource(rr, "/apps/validation/1/resource", JcrConstants.NT_UNSTRUCTURED,
JcrConstants.NT_UNSTRUCTURED, true);
@@ -401,8 +506,8 @@ public class ValidationServiceImplTest {
if (rr != null) {
model1 = createValidationModelResource(rr, libsValidatorsRoot.getPath(), "testValidationModel1", "sling/validation/test",
new String[]{"/apps/validation"}, property);
- Resource child = createValidationModelChildResource(model1, "child1", "child.*", new TestProperty("hello"));
- createValidationModelChildResource(child, "grandChild", "grandChild.*", new TestProperty("hello"));
+ Resource child = createValidationModelChildResource(model1, "child1", "child.*", false, new TestProperty("hello"));
+ createValidationModelChildResource(child, "grandChild", "grandChild.*", false, new TestProperty("hello"));
rr.commit();
testResource = ResourceUtil.getOrCreateResource(rr, "/apps/validation/1/resource", JcrConstants.NT_UNSTRUCTURED,
@@ -560,10 +665,12 @@ public class ValidationServiceImplTest {
Resource propertyResource = ResourceUtil.getOrCreateResource(rr, propertiesResource.getPath() + "/" + property.name,
modelPropertyJCRProperties, null, true);
if (propertyResource != null) {
+ ModifiableValueMap values = propertyResource.adaptTo(ModifiableValueMap.class);
if (property.nameRegex != null) {
- ModifiableValueMap values = propertyResource.adaptTo(ModifiableValueMap.class);
values.put(Constants.NAME_REGEX, property.nameRegex);
}
+ values.put(Constants.PROPERTY_MULTIPLE, property.multiple);
+ values.put(Constants.OPTIONAL, property.optional);
Resource validators = ResourceUtil.getOrCreateResource(rr,
propertyResource.getPath() + "/" + Constants.VALIDATORS,
JcrConstants.NT_UNSTRUCTURED, null, true);
@@ -583,7 +690,7 @@ public class ValidationServiceImplTest {
}
}
- private Resource createValidationModelChildResource(Resource parentResource, String name, String nameRegex, TestProperty... properties) throws PersistenceException {
+ private Resource createValidationModelChildResource(Resource parentResource, String name, String nameRegex, boolean isOptional, TestProperty... properties) throws PersistenceException {
ResourceResolver rr = parentResource.getResourceResolver();
Resource modelChildren = rr.create(parentResource, Constants.CHILDREN, new HashMap<String, Object>(){{
put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED);
@@ -591,10 +698,11 @@ public class ValidationServiceImplTest {
Resource child = rr.create(modelChildren, name, new HashMap<String, Object>(){{
put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED);
}});
+ ModifiableValueMap mvm = child.adaptTo(ModifiableValueMap.class);
if (nameRegex != null) {
- ModifiableValueMap mvm = child.adaptTo(ModifiableValueMap.class);
mvm.put(Constants.NAME_REGEX, nameRegex);
}
+ mvm.put(Constants.OPTIONAL, isOptional);
createValidationModelProperties(child, properties);
return child;
}
@@ -613,6 +721,8 @@ public class ValidationServiceImplTest {
}
private class TestProperty {
+ public boolean optional;
+ public boolean multiple;
final String name;
String nameRegex;
final Map<String, String[]> validators;
@@ -621,6 +731,8 @@ public class ValidationServiceImplTest {
validators = new HashMap<String, String[]>();
this.name = name;
this.nameRegex = null;
+ this.optional = false;
+ this.multiple = false;
}
TestProperty setNameRegex(String nameRegex) {