/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.sling.validation.impl.resourcemodel; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nonnull; import javax.jcr.Node; import javax.jcr.RepositoryException; import javax.jcr.Session; import org.apache.jackrabbit.JcrConstants; import org.apache.jackrabbit.util.Text; import org.apache.sling.api.resource.LoginException; import org.apache.sling.api.resource.ModifiableValueMap; import org.apache.sling.api.resource.PersistenceException; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ResourceResolverFactory; import org.apache.sling.api.resource.ResourceUtil; import org.apache.sling.api.resource.ValueMap; import org.apache.sling.jcr.resource.JcrResourceConstants; import org.apache.sling.testing.mock.jcr.MockJcr; import org.apache.sling.testing.mock.jcr.MockQuery; import org.apache.sling.testing.mock.jcr.MockQueryResult; import org.apache.sling.testing.mock.jcr.MockQueryResultHandler; import org.apache.sling.testing.mock.sling.ResourceResolverType; import org.apache.sling.testing.mock.sling.junit.SlingContext; import org.apache.sling.validation.impl.model.ChildResourceImpl; import org.apache.sling.validation.impl.model.ResourcePropertyBuilder; import org.apache.sling.validation.impl.model.ValidationModelBuilder; import org.apache.sling.validation.impl.validators.RegexValidator; import org.apache.sling.validation.model.ChildResource; import org.apache.sling.validation.model.ValidatorInvocation; import org.apache.sling.validation.model.ResourceProperty; import org.apache.sling.validation.model.ValidationModel; import org.hamcrest.Matchers; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class ResourceValidationModelProviderImplTest { private final static class PrefixAndResourceType { private final String prefix; private final String resourceType; public PrefixAndResourceType(String prefix, String resourceType) { super(); this.prefix = prefix; this.resourceType = resourceType; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((prefix == null) ? 0 : prefix.hashCode()); result = prime * result + ((resourceType == null) ? 0 : resourceType.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; PrefixAndResourceType other = (PrefixAndResourceType) obj; if (prefix == null) { if (other.prefix != null) return false; } else if (!prefix.equals(other.prefix)) return false; if (resourceType == null) { if (other.resourceType != null) return false; } else if (!resourceType.equals(other.resourceType)) return false; return true; } } /** * Assume the validation models are stored under (/libs|/apps) + / + VALIDATION_MODELS_RELATIVE_PATH. */ private static final String VALIDATION_MODELS_RELATIVE_PATH = "sling/validation/models"; private static final String APPS = "/apps"; private static final String LIBS = "/libs"; private Resource appsValidatorsRoot; private Resource libsValidatorsRoot; private static Map<String, Object> primaryTypeUnstructuredMap; private ResourceValidationModelProviderImpl modelProvider; private ResourceResolver rr; @Mock private ResourceResolverFactory resourceResolverFactory; private MockQueryResultHandler prefixBasedResultHandler; private Map<PrefixAndResourceType, List<Node>> validatorModelNodesPerPrefixAndResourceType; private ValidationModelBuilder modelBuilder; // extract resource type from strings like // "/jcr:root/apps//validation//*[@sling:resourceType="sling/validation/model" and @validatedResourceType="<some-resource-type>"]" private static final Pattern RESOURCE_TYPE_PATTERN = Pattern.compile(".*@validatingResourceType=\"([^\"]*)\".*"); @Rule public SlingContext context = new SlingContext(ResourceResolverType.JCR_MOCK); // search capability necessary @Before public void setUp() throws LoginException, PersistenceException, RepositoryException { primaryTypeUnstructuredMap = new HashMap<String, Object>(); primaryTypeUnstructuredMap.put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED); modelProvider = new ResourceValidationModelProviderImpl(); // one default model modelBuilder = new ValidationModelBuilder(); modelBuilder.setApplicablePath("/content/site1"); ResourcePropertyBuilder propertyBuilder = new ResourcePropertyBuilder(); propertyBuilder.validator("validatorId", 10, RegexValidator.REGEX_PARAM, "prefix.*"); ResourceProperty property = propertyBuilder.build("field1"); modelBuilder.resourceProperty(property); prefixBasedResultHandler = new MockQueryResultHandler() { @Override public MockQueryResult executeQuery(MockQuery query) { if (!"xpath".equals(query.getLanguage())) { return null; } String statement = query.getStatement(); // query looks like /jcr:root/apps//validation//*[@sling:resourceType="sling/validation/model" and // @validatingResourceType="<some-resource-type>"] if (statement.startsWith("/jcr:root/")) { statement = statement.substring("/jcr:root/".length() - 1); } // extract the prefix from the statement String prefix = Text.getAbsoluteParent(statement, 0); // extract the resource type from the statement Matcher matcher = RESOURCE_TYPE_PATTERN.matcher(statement); if (!matcher.matches()) { throw new IllegalArgumentException( "Can only process query statements which contain a validatedResourceType but the statement is: " + statement); } String resourceType = matcher.group(1); PrefixAndResourceType prefixAndResourceType = new PrefixAndResourceType(prefix, resourceType); if (validatorModelNodesPerPrefixAndResourceType.keySet().contains(prefixAndResourceType)) { return new MockQueryResult(validatorModelNodesPerPrefixAndResourceType.get(prefixAndResourceType)); } return null; } }; rr = context.resourceResolver(); modelProvider.rrf = resourceResolverFactory; // create a wrapper resource resolver, which cannot be closed (as the SlingContext will take care of that) ResourceResolver nonClosableResourceResolverWrapper = Mockito.spy(rr); // intercept all close calls Mockito.doNothing().when(nonClosableResourceResolverWrapper).close(); // always use the context's resource resolver (because we never commit) Mockito.when(resourceResolverFactory.getServiceResourceResolver(Mockito.anyObject())).thenReturn(nonClosableResourceResolverWrapper); MockJcr.addQueryResultHandler(rr.adaptTo(Session.class), prefixBasedResultHandler); validatorModelNodesPerPrefixAndResourceType = new HashMap<PrefixAndResourceType, List<Node>>(); appsValidatorsRoot = ResourceUtil.getOrCreateResource(rr, APPS + "/" + VALIDATION_MODELS_RELATIVE_PATH, (Map<String, Object>) null, "sling:Folder", true); libsValidatorsRoot = ResourceUtil.getOrCreateResource(rr, LIBS + "/" + VALIDATION_MODELS_RELATIVE_PATH, (Map<String, Object>) null, "sling:Folder", true); } @After public void tearDown() throws PersistenceException { if (appsValidatorsRoot != null) { rr.delete(appsValidatorsRoot); } if (libsValidatorsRoot != null) { rr.delete(libsValidatorsRoot); } rr.commit(); } @Test public void testGetValidationModels() throws Exception { // build two models manually (which are identical except for the applicable path) ResourcePropertyBuilder resourcePropertyBuilder = new ResourcePropertyBuilder(); ValidationModel model1 = modelBuilder.resourceProperty(resourcePropertyBuilder.build("property1")).build("sling/validation/test", libsValidatorsRoot.getPath() + "/testValidationModel1"); modelBuilder.setApplicablePath("/content/site2"); ValidationModel model2 = modelBuilder.build("sling/validation/test", libsValidatorsRoot.getPath() + "/testValidationModel2"); // build models in JCR createValidationModelResource(rr, libsValidatorsRoot.getPath(), "testValidationModel1", model1); createValidationModelResource(rr, libsValidatorsRoot.getPath(), "testValidationModel2", model2); // check that both models are returned Collection<ValidationModel> models = modelProvider.getValidationModels("sling/validation/test"); Assert.assertThat(models, Matchers.containsInAnyOrder(model1, model2)); } @Test public void testGetValidationModelsWithoutApplicablePath() throws Exception { modelBuilder = new ValidationModelBuilder(); // build two models manually (which are identical except for the applicable path) ResourcePropertyBuilder resourcePropertyBuilder = new ResourcePropertyBuilder(); ValidationModel model1 = modelBuilder.resourceProperty(resourcePropertyBuilder.build("property1")).build("sling/validation/test", libsValidatorsRoot.getPath() + "/testValidationModel1"); // build models in JCR Resource modelResource = createValidationModelResource(rr, libsValidatorsRoot.getPath(), "testValidationModel1", model1); ModifiableValueMap properties = modelResource.adaptTo(ModifiableValueMap.class); properties.remove("applicablePaths"); // check that both models are returned Collection<ValidationModel> models = modelProvider.getValidationModels("sling/validation/test"); Assert.assertThat(models, Matchers.containsInAnyOrder(model1)); } @Test public void testGetValidationModelsWithEmptyApplicablePath() throws Exception { modelBuilder = new ValidationModelBuilder(); // build two models manually (which are identical except for the applicable path) ResourcePropertyBuilder resourcePropertyBuilder = new ResourcePropertyBuilder(); ValidationModel model1 = modelBuilder.resourceProperty(resourcePropertyBuilder.build("property1")).build("sling/validation/test", libsValidatorsRoot.getPath() + "/testValidationModel1"); // build models in JCR createValidationModelResource(rr, libsValidatorsRoot.getPath(), "testValidationModel1", model1); // check that both models are returned Collection<ValidationModel> models = modelProvider.getValidationModels("sling/validation/test"); Assert.assertThat(models, Matchers.containsInAnyOrder(model1)); } @Test public void testGetValidationModelsOutsideSearchPath() throws Exception { // build two models manually (which are identical except for the applicable path) ValidationModel model1 = modelBuilder.build("sling/validation/test", "some source"); Resource contentValidatorsRoot = ResourceUtil.getOrCreateResource(rr, "/content", (Map<String, Object>) null, "sling:Folder", true); try { // build models in JCR outside any search path /apps or /libs createValidationModelResource(rr, contentValidatorsRoot.getPath(), "testValidationModel1", model1); // check that no model is found Collection<ValidationModel> models = modelProvider.getValidationModels("sling/validation/test"); Assert.assertThat("Model was placed outside resource resolver search path but still found", models, Matchers.empty()); } finally { rr.delete(contentValidatorsRoot); } } @Test public void testGetValidationModelsWithChildren() throws Exception { // build two models manually (which are identical except for the applicable path) ResourcePropertyBuilder resourcePropertyBuilder = new ResourcePropertyBuilder(); resourcePropertyBuilder.multiple(); resourcePropertyBuilder.optional(); ResourceProperty childproperty = resourcePropertyBuilder.build("child1property"); modelBuilder.childResource(new ChildResourceImpl("child1", null, true, Collections.singletonList(childproperty), Collections.<ChildResource> emptyList())); ValidationModel model1 = modelBuilder.build("sling/validation/test", libsValidatorsRoot.getPath() + "/testValidationModel1"); // build models in JCR createValidationModelResource(rr, libsValidatorsRoot.getPath(), "testValidationModel1", model1); // compare both models Collection<ValidationModel> models = modelProvider.getValidationModels("sling/validation/test"); Assert.assertThat(models, Matchers.contains(model1)); } @Test public void testGetValidationModelsWithOverlay() throws Exception { // create two models manually (which are identical except for the applicable path) ValidationModel model1 = modelBuilder.build("sling/validation/test", libsValidatorsRoot.getPath() + "/testValidationModel1"); modelBuilder.setApplicablePath("/content/site2"); ValidationModel model2 = modelBuilder.build("sling/validation/test", appsValidatorsRoot.getPath() + "/testValidationModel1"); // create two models: one in libs and one in apps (distinguishable via applicablePath) createValidationModelResource(rr, libsValidatorsRoot.getPath(), "testValidationModel1", model1); createValidationModelResource(rr, appsValidatorsRoot.getPath(), "testValidationModel1", model2); // only the apps model should be returned Collection<ValidationModel> models = modelProvider.getValidationModels("sling/validation/test"); Assert.assertThat(models, Matchers.contains(model2)); } @Test(expected = IllegalStateException.class) public void testGetValidationModelsWithMissingChildrenAndProperties() throws Exception { // create a model with properties (otherwise build() will already throw an exception) modelBuilder = new ValidationModelBuilder(); modelBuilder.resourceProperty(new ResourcePropertyBuilder().build("field1")); modelBuilder.addApplicablePath("content/site1"); ValidationModel model1 = modelBuilder.build("sling/validation/test", libsValidatorsRoot.getPath() + "/testValidationModel1"); Resource resource = createValidationModelResource(rr, libsValidatorsRoot.getPath(), "testValidationModel1", model1); // make created model invalid by removing the properties sub resource rr.delete(resource.getChild("properties")); modelProvider.getValidationModels("sling/validation/test"); } @Test public void testGetValidationModelsWithComplexValidatorArguments() throws Exception { // create a model with neither children nor properties Map<String, Object> validatorArguments = new HashMap<>(); validatorArguments.put("key1", "value1"); validatorArguments.put("key1", "value2"); validatorArguments.put("key1", "value3"); validatorArguments.put("key2", "value1"); validatorArguments.put("key3", "value1=value2"); modelBuilder = new ValidationModelBuilder(); modelBuilder.resourceProperty(new ResourcePropertyBuilder().validator("validatorId", 10, validatorArguments).build("field1")); modelBuilder.addApplicablePath("content/site1"); ValidationModel model1 = modelBuilder.build("sling/validation/test", libsValidatorsRoot.getPath() + "/testValidationModel1"); createValidationModelResource(rr, libsValidatorsRoot.getPath(), "testValidationModel1", model1); Collection<ValidationModel> models = modelProvider.getValidationModels("sling/validation/test"); Assert.assertThat(models, Matchers.contains(model1)); } @Test(expected=IllegalStateException.class) public void testGetValidationModelsWithInvalidValidatorArguments1() throws Exception { // create a model with neither children nor properties ValidationModel model1 = modelBuilder.build("sling/validation/test", libsValidatorsRoot.getPath() + "/testValidationModel1"); // create valid model first Resource modelResource = createValidationModelResource(rr, libsValidatorsRoot.getPath(), "testValidationModel1", model1); // and make parametrization of validator invalid afterwards Resource validatorResource = modelResource.getChild("properties/field1/validators/validatorId"); ModifiableValueMap validatorArguments = validatorResource.adaptTo(ModifiableValueMap.class); validatorArguments.put("validatorArguments", "key1"); // value without "=" Collection<ValidationModel> models = modelProvider.getValidationModels("sling/validation/test"); Assert.assertThat(models, Matchers.contains(model1)); } @Test(expected=IllegalStateException.class) public void testGetValidationModelsWithInvalidValidatorArguments2() throws Exception { // create a model with neither children nor properties ValidationModel model1 = modelBuilder.build("sling/validation/test", libsValidatorsRoot.getPath() + "/testValidationModel1"); // create valid model first Resource modelResource = createValidationModelResource(rr, libsValidatorsRoot.getPath(), "testValidationModel1", model1); // and make parametrization of validator invalid afterwards Resource validatorResource = modelResource.getChild("properties/field1/validators/validatorId"); ModifiableValueMap validatorArguments = validatorResource.adaptTo(ModifiableValueMap.class); validatorArguments.put("validatorArguments", "=value2"); // starting with "=" Collection<ValidationModel> models = modelProvider.getValidationModels("sling/validation/test"); Assert.assertThat(models, Matchers.contains(model1)); } @Test(expected=IllegalStateException.class) public void testGetValidationModelsWithInvalidValidatorArguments3() throws Exception { // create a model with neither children nor properties ValidationModel model1 = modelBuilder.build("sling/validation/test", libsValidatorsRoot.getPath() + "/testValidationModel1"); // create valid model first Resource modelResource = createValidationModelResource(rr, libsValidatorsRoot.getPath(), "testValidationModel1", model1); // and make parametrization of validator invalid afterwards Resource validatorResource = modelResource.getChild("properties/field1/validators/validatorId"); ModifiableValueMap validatorArguments = validatorResource.adaptTo(ModifiableValueMap.class); validatorArguments.put("validatorArguments", "key1="); // ending with "=" Collection<ValidationModel> models = modelProvider.getValidationModels("sling/validation/test"); Assert.assertThat(models, Matchers.contains(model1)); } @Test public void testCachingOfGetValidationModels() throws Exception { // build one model ResourcePropertyBuilder resourcePropertyBuilder = new ResourcePropertyBuilder(); ValidationModel model1 = modelBuilder.resourceProperty(resourcePropertyBuilder.build("property1")).build("sling/validation/test", libsValidatorsRoot.getPath() + "/testValidationModel1"); modelBuilder.setApplicablePath("/content/site2"); // build models in JCR createValidationModelResource(rr, libsValidatorsRoot.getPath(), "testValidationModel1", model1); // check that both models are returned Collection<ValidationModel> models = modelProvider.getValidationModels("sling/validation/test"); Assert.assertThat(models, Matchers.containsInAnyOrder(model1)); // the 2nd time the same instance should be returned Collection<ValidationModel> models2 = modelProvider.getValidationModels("sling/validation/test"); Assert.assertEquals("Due to caching both models should be actually the same instance", System.identityHashCode(models), System.identityHashCode(models2)); } /*--- the following methods create validation model resources from ValidationModel objects --*/ private Resource createValidationModelResource(ResourceResolver rr, String root, String name, ValidationModel model) throws Exception { Map<String, Object> modelProperties = new HashMap<String, Object>(); modelProperties.put(ResourceValidationModelProviderImpl.VALIDATING_RESOURCE_TYPE, model.getValidatingResourceType()); modelProperties.put(ResourceValidationModelProviderImpl.APPLICABLE_PATHS, model.getApplicablePaths().toArray()); modelProperties.put(JcrResourceConstants.SLING_RESOURCE_TYPE_PROPERTY, ResourceValidationModelProviderImpl.VALIDATION_MODEL_RESOURCE_TYPE); modelProperties.put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED); Resource modelResource = ResourceUtil.getOrCreateResource(rr, root + "/" + name, modelProperties, JcrResourceConstants.NT_SLING_FOLDER, true); if (model != null) { createValidationModelProperties(modelResource, model.getResourceProperties()); for (ChildResource child : model.getChildren()) { createValidationModelChildResource(modelResource, child); } // add to search handler (with root path) String prefix = Text.getAbsoluteParent(root, 0); PrefixAndResourceType prefixAndResourceType = new PrefixAndResourceType(prefix, model.getValidatingResourceType()); List<Node> nodes; nodes = validatorModelNodesPerPrefixAndResourceType.get(prefixAndResourceType); if (nodes == null) { nodes = new ArrayList<Node>(); validatorModelNodesPerPrefixAndResourceType.put(prefixAndResourceType, nodes); } nodes.add(modelResource.adaptTo(Node.class)); } return modelResource; } /** * Always uses the validator's class name as validator resource name. * @param model * @param properties * @throws PersistenceException */ private void createValidationModelProperties(Resource model, @Nonnull Collection<ResourceProperty> properties) throws PersistenceException { ResourceResolver rr = model.getResourceResolver(); if (properties.isEmpty()) { return; } Resource propertiesResource = ResourceUtil.getOrCreateResource(rr, model.getPath() + "/" + ResourceValidationModelProviderImpl.PROPERTIES, JcrConstants.NT_UNSTRUCTURED, null, true); for (ResourceProperty property : properties) { Map<String, Object> modelPropertyJCRProperties = new HashMap<String, Object>(); modelPropertyJCRProperties.put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED); Resource propertyResource = ResourceUtil.getOrCreateResource(rr, propertiesResource.getPath() + "/" + property.getName(), modelPropertyJCRProperties, null, true); if (propertyResource != null) { ModifiableValueMap values = propertyResource.adaptTo(ModifiableValueMap.class); Pattern pattern = property.getNamePattern(); if (pattern != null) { values.put(ResourceValidationModelProviderImpl.NAME_REGEX, pattern.pattern()); } values.put(ResourceValidationModelProviderImpl.PROPERTY_MULTIPLE, property.isMultiple()); values.put(ResourceValidationModelProviderImpl.OPTIONAL, !property.isRequired()); Resource validators = ResourceUtil.getOrCreateResource(rr, propertyResource.getPath() + "/" + ResourceValidationModelProviderImpl.VALIDATORS, JcrConstants.NT_UNSTRUCTURED, null, true); if (validators != null) { for (ValidatorInvocation validatorIncovation : property.getValidatorInvocations()) { Map<String, Object> validatorProperties = new HashMap<String, Object>(); validatorProperties.put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED); ValueMap parameters = validatorIncovation.getParameters(); if (!parameters.isEmpty()) { // convert to right format validatorProperties.put(ResourceValidationModelProviderImpl.VALIDATOR_ARGUMENTS, convertMapToJcrValidatorArguments(parameters)); } Integer severity = validatorIncovation.getSeverity(); if (severity != null) { validatorProperties.put(ResourceValidationModelProviderImpl.SEVERITY, severity); } ResourceUtil.getOrCreateResource(rr, validators.getPath() + "/" + validatorIncovation.getValidatorId(), validatorProperties, null, true); } } } } } /** * Convert to right format : String array of "<key>=<value>" * * @param map * @return */ private String[] convertMapToJcrValidatorArguments(ValueMap map) { List<String> parametersForJcr = new ArrayList<String>(); for (Map.Entry<String, Object> entry : map.entrySet()) { if (entry.getValue() instanceof String[]) { for (String value : (String[]) entry.getValue()) { parametersForJcr.add(value + "=" + entry.getValue()); } } else { parametersForJcr.add(entry.getKey() + "=" + entry.getValue()); } } return parametersForJcr.toArray(new String[0]); } private Resource createValidationModelChildResource(Resource parentResource, ChildResource child) throws PersistenceException { ResourceResolver rr = parentResource.getResourceResolver(); Resource modelChildren = rr.create(parentResource, ResourceValidationModelProviderImpl.CHILDREN, primaryTypeUnstructuredMap); Resource modelResource = rr.create(modelChildren, child.getName(), primaryTypeUnstructuredMap); ModifiableValueMap mvm = modelResource.adaptTo(ModifiableValueMap.class); if (child.getNamePattern() != null) { mvm.put(ResourceValidationModelProviderImpl.NAME_REGEX, child.getNamePattern() ); } mvm.put(ResourceValidationModelProviderImpl.OPTIONAL, !child.isRequired()); createValidationModelProperties(modelResource, child.getProperties()); // recursion for all childs for (ChildResource grandChild : child.getChildren()) { createValidationModelChildResource(modelResource, grandChild); } return modelResource; } }