/* * 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.Arrays; import java.util.Dictionary; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import javax.annotation.Nonnull; import org.apache.sling.api.SlingConstants; import org.apache.sling.api.resource.LoginException; 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.ValueMap; import org.apache.sling.serviceusermapping.ServiceUserMapped; 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.model.ChildResource; import org.apache.sling.validation.model.ResourceProperty; import org.apache.sling.validation.model.ValidationModel; import org.apache.sling.validation.model.spi.ValidationModelProvider; import org.osgi.framework.ServiceRegistration; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Reference; import org.osgi.service.event.Event; import org.osgi.service.event.EventConstants; import org.osgi.service.event.EventHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; //the event handler is dynamic and registered in the activate method @Component(service = ValidationModelProvider.class) public class ResourceValidationModelProviderImpl implements ValidationModelProvider, EventHandler { static final String MODEL_XPATH_QUERY = "/jcr:root%s/*[@sling:resourceType=\"" + ResourceValidationModelProviderImpl.VALIDATION_MODEL_RESOURCE_TYPE + "\" and @" + ResourceValidationModelProviderImpl.VALIDATING_RESOURCE_TYPE + "=\"%s\"]"; static final String[] TOPICS = { SlingConstants.TOPIC_RESOURCE_REMOVED, SlingConstants.TOPIC_RESOURCE_CHANGED, SlingConstants.TOPIC_RESOURCE_ADDED }; public static final @Nonnull String NAME_REGEX = "nameRegex"; public static final @Nonnull String CHILDREN = "children"; public static final @Nonnull String VALIDATOR_ARGUMENTS = "validatorArguments"; public static final @Nonnull String VALIDATORS = "validators"; public static final @Nonnull String OPTIONAL = "optional"; public static final @Nonnull String PROPERTY_MULTIPLE = "propertyMultiple"; public static final @Nonnull String PROPERTIES = "properties"; public static final @Nonnull String VALIDATION_MODEL_RESOURCE_TYPE = "sling/validation/model"; public static final @Nonnull String APPLICABLE_PATHS = "applicablePaths"; public static final @Nonnull String VALIDATING_RESOURCE_TYPE = "validatingResourceType"; public static final @Nonnull String SEVERITY = "severity"; @Reference ResourceResolverFactory rrf = null; private static final Logger LOG = LoggerFactory.getLogger(ResourceValidationModelProviderImpl.class); private ServiceRegistration<EventHandler> eventHandlerRegistration; @Reference private ServiceUserMapped serviceUserMapped; /** key = resource type, value = a list of all validation models for the resource type given in the key */ final Map<String, List<ValidationModel>> validationModelCacheByResourceType = new ConcurrentHashMap<>(); @Activate protected void activate(ComponentContext componentContext) throws LoginException { ResourceResolver rr = null; try { rr = rrf.getServiceResourceResolver(null); StringBuilder sb = new StringBuilder("("); String[] searchPaths = rr.getSearchPath(); if (searchPaths.length > 1) { sb.append("|"); } for (String searchPath : searchPaths) { sb.append("(path=").append(searchPath + "*").append(")"); } sb.append(")"); Dictionary<String, Object> eventHandlerProperties = new Hashtable<String, Object>(); eventHandlerProperties.put(EventConstants.EVENT_TOPIC, TOPICS); eventHandlerProperties.put(EventConstants.EVENT_FILTER, sb.toString()); eventHandlerRegistration = componentContext.getBundleContext().registerService( EventHandler.class, this, eventHandlerProperties); LOG.debug("Registered event handler for validation models in {}", sb.toString()); } finally { if (rr != null) { rr.close(); } } } @Deactivate protected void deactivate(ComponentContext componentContext) { if (eventHandlerRegistration != null) { eventHandlerRegistration.unregister(); eventHandlerRegistration = null; } } @Override public void handleEvent(Event event) { String path = (String) event.getProperty(SlingConstants.PROPERTY_PATH); if (path == null) { LOG.warn("Received event {}, but could not get the affected path", event); return; } Set<String> resourceTypesToInvalidate = new HashSet<>(); switch (event.getTopic()) { case SlingConstants.TOPIC_RESOURCE_REMOVED: // find cache entries below the removed resource for (Entry<String, List<ValidationModel>> validationModelByResourceType : validationModelCacheByResourceType.entrySet()) { for (ValidationModel model : validationModelByResourceType.getValue()) { if (model.getSource().startsWith(path)) { LOG.debug("Invalidate validation model at {}, because resource at {} has been removed", model.getSource(), path); resourceTypesToInvalidate.add(validationModelByResourceType.getKey()); } } } break; default: // only consider additions/changes of resources with resource type = validation model resource type String resourceType = (String) event.getProperty(SlingConstants.PROPERTY_RESOURCE_TYPE); if (resourceType == null) { LOG.warn("Received event {}, but could not get the modified/added resource type", event); return; } if (VALIDATION_MODEL_RESOURCE_TYPE.equals(resourceType)) { // retrieve the resource types covered by the newly added model String resourceTypeToInvalidate = null; try { resourceTypeToInvalidate = getResourceTypeOfValidationModel(path); } catch (Exception e) { LOG.warn("Could not get covered resource type of newly added validation model at " + path, e); } if (resourceTypeToInvalidate != null) { LOG.debug( "Invalidate validation models for resource type {}, because resource at {} provides a new/modified validation model for that type", resourceType, path); resourceTypesToInvalidate.add(resourceTypeToInvalidate); } else { LOG.debug("Resource at {} provides a new/modified validation model but could not yet determine for which resource type", path); } } // or paths already covered by the cache for (Entry<String, List<ValidationModel>> validationModelByResourceType : validationModelCacheByResourceType.entrySet()) { for (ValidationModel model : validationModelByResourceType.getValue()) { if (path.startsWith(model.getSource())) { LOG.debug("Invalidate validation model at {}, because resource below (at {}) has been modified", model.getSource(), path); resourceTypesToInvalidate.add(validationModelByResourceType.getKey()); } } } } for (String resourceTypeToInvalidate : resourceTypesToInvalidate) { validationModelCacheByResourceType.remove(resourceTypeToInvalidate); } } private String getResourceTypeOfValidationModel(@Nonnull String path) throws LoginException { ResourceResolver resourceResolver = null; try { resourceResolver = rrf.getServiceResourceResolver(null); Resource modelResource = resourceResolver.getResource(path); if (modelResource == null) { throw new IllegalStateException("Can no longer access resource at " + path); } ValueMap properties = modelResource.adaptTo(ValueMap.class); if (properties == null) { throw new IllegalStateException("Could not adapt resource at " + path + " to a ValueMap"); } return properties.get(VALIDATING_RESOURCE_TYPE, String.class); } finally { if (resourceResolver != null) { resourceResolver.close(); } } } /* * (non-Javadoc) * * @see org.apache.sling.validation.model.spi.ValidationModelProvider#getModels(java.lang.String) */ @Override public @Nonnull List<ValidationModel> getValidationModels(@Nonnull String relativeResourceType) { List<ValidationModel> cacheEntry = validationModelCacheByResourceType.get(relativeResourceType); if (cacheEntry == null) { cacheEntry = doGetModels(relativeResourceType); validationModelCacheByResourceType.put(relativeResourceType, cacheEntry); } else { LOG.debug("Found entry in cache for resource type {}", relativeResourceType); } return cacheEntry; } /** Searches for validation models bound to a specific resource type through a search query. * * @param relativeResourceType the resource type to look for * @return a List of {@link ValidationModel}s. Never {@code null}, but might be empty collection in case no model for the given resource * type could be found. Returns the models below "/apps" before the models below "/libs". * @throws IllegalStateException in case a validation model is found but it is invalid */ @Nonnull private List<ValidationModel> doGetModels(@Nonnull String relativeResourceType) { List<ValidationModel> validationModels = new ArrayList<ValidationModel>(); ResourceResolver resourceResolver = null; try { resourceResolver = rrf.getServiceResourceResolver(null); String[] searchPaths = resourceResolver.getSearchPath(); for (String searchPath : searchPaths) { final String queryString = String.format(MODEL_XPATH_QUERY, searchPath, relativeResourceType); LOG.debug("Looking for validation models with query '{}'", queryString); Iterator<Resource> models = resourceResolver.findResources(queryString, "xpath"); while (models.hasNext()) { Resource model = models.next(); LOG.debug("Found validation model resource {}.", model.getPath()); String resourcePath = model.getPath(); try { ValidationModelBuilder modelBuilder = new ValidationModelBuilder(); ValueMap validationModelProperties = model.getValueMap(); modelBuilder.addApplicablePaths(validationModelProperties.get(ResourceValidationModelProviderImpl.APPLICABLE_PATHS, new String[] {})); Resource propertiesResource = model.getChild(ResourceValidationModelProviderImpl.PROPERTIES); modelBuilder.resourceProperties(buildProperties(propertiesResource)); modelBuilder.childResources(buildChildren(model, model)); ValidationModel vm = modelBuilder.build(relativeResourceType, resourcePath); validationModels.add(vm); } catch (IllegalArgumentException e) { throw new IllegalStateException("Found invalid validation model in '" + resourcePath + "': " + e.getMessage(), e); } } if (!validationModels.isEmpty()) { // do not continue to search in other search paths if some results were already found! // earlier search paths overlay lower search paths (/apps wins over /libs) // the applicable content paths do not matter here! break; } } return validationModels; } catch (LoginException e) { throw new IllegalStateException("Could not get service resource resolver", e); } finally { if (resourceResolver != null) { resourceResolver.close(); } } } /** Creates a set of the properties that a resource is expected to have, together with the associated validators. * * @param propertiesResource the resource identifying the properties node from a validation model's structure (might be {@code null}) * @return a set of properties or an empty set if no properties are defined * @see ResourceProperty */ private @Nonnull List<ResourceProperty> buildProperties(@Nonnull Resource propertiesResource) { List<ResourceProperty> properties = new ArrayList<ResourceProperty>(); if (propertiesResource != null) { for (Resource propertyResource : propertiesResource.getChildren()) { ResourcePropertyBuilder resourcePropertyBuilder = new ResourcePropertyBuilder(); String fieldName = propertyResource.getName(); ValueMap propertyValueMap = propertyResource.getValueMap(); if (propertyValueMap.get(ResourceValidationModelProviderImpl.PROPERTY_MULTIPLE, false)) { resourcePropertyBuilder.multiple(); } if (propertyValueMap.get(ResourceValidationModelProviderImpl.OPTIONAL, false)) { resourcePropertyBuilder.optional(); } String nameRegex = propertyValueMap.get(ResourceValidationModelProviderImpl.NAME_REGEX, String.class); if (nameRegex != null) { resourcePropertyBuilder.nameRegex(nameRegex); } Resource validators = propertyResource.getChild(ResourceValidationModelProviderImpl.VALIDATORS); if (validators != null) { Iterator<Resource> validatorsIterator = validators.listChildren(); while (validatorsIterator.hasNext()) { Resource validatorResource = validatorsIterator.next(); ValueMap validatorProperties = validatorResource.adaptTo(ValueMap.class); if (validatorProperties == null) { throw new IllegalStateException( "Could not adapt resource at '" + validatorResource.getPath() + "' to ValueMap"); } String validatorId = validatorResource.getName(); // get arguments for validator String[] validatorArguments = validatorProperties.get(ResourceValidationModelProviderImpl.VALIDATOR_ARGUMENTS, String[].class); Map<String, Object> validatorArgumentsMap = new HashMap<String, Object>(); if (validatorArguments != null) { for (String arg : validatorArguments) { int positionOfSeparator = arg.indexOf("="); if (positionOfSeparator < 1 || positionOfSeparator >= arg.length()-1) { throw new IllegalArgumentException("Invalid validator argument '" + arg + "' found, because it does not follow the format '<key>=<value>'"); } String key = arg.substring(0, positionOfSeparator); String value = arg.substring(positionOfSeparator + 1); Object newValue; if (validatorArgumentsMap.containsKey(key)) { Object oldValue = validatorArgumentsMap.get(key); // either extend old array if (oldValue instanceof String[]) { String[] oldArray = (String[]) oldValue; int newLength = oldArray.length + 1; String[] newArray = Arrays.copyOf(oldArray, oldArray.length + 1); newArray[newLength - 1] = value; newValue = newArray; } else { // or turn the single value into a multi-value array newValue = new String[] { (String) oldValue, value }; } } else { newValue = value; } validatorArgumentsMap.put(key, newValue); } } // get severity Integer severity = validatorProperties.get(SEVERITY, Integer.class); resourcePropertyBuilder.validator(validatorId, severity, validatorArgumentsMap); } } properties.add(resourcePropertyBuilder.build(fieldName)); } } return properties; } /** Searches children resources from a {@code modelResource}, starting from the {@code rootResource}. If one needs all the children * resources of a model, then the {@code modelResource} and the {@code rootResource} should be identical. * * @param modelResource the resource describing a {@link org.apache.sling.validation.api.ValidationModel} * @param rootResource the model's resource from which to search for children (this resource has to have a * {@link ResourceValidationModelProviderImpl#CHILDREN} node directly underneath it) * @return a list of all the children resources; the list will be empty if there are no children resources */ private @Nonnull List<ChildResource> buildChildren(@Nonnull Resource modelResource, @Nonnull Resource rootResource) { List<ChildResource> children = new ArrayList<ChildResource>(); Resource childrenResource = rootResource.getChild(ResourceValidationModelProviderImpl.CHILDREN); if (childrenResource != null) { for (Resource child : childrenResource.getChildren()) { // if pattern is set, always use that ValueMap childrenProperties = child.adaptTo(ValueMap.class); if (childrenProperties == null) { throw new IllegalStateException("Could not adapt resource " + child.getPath() + " to ValueMap"); } final String name = child.getName(); final String nameRegex; if (childrenProperties.containsKey(ResourceValidationModelProviderImpl.NAME_REGEX)) { nameRegex = childrenProperties.get(ResourceValidationModelProviderImpl.NAME_REGEX, String.class); } else { // otherwise fall back to the name nameRegex = null; } boolean isRequired = !childrenProperties.get(ResourceValidationModelProviderImpl.OPTIONAL, false); ChildResource childResource = new ChildResourceImpl(name, nameRegex, isRequired, buildProperties(child.getChild(ResourceValidationModelProviderImpl.PROPERTIES)), buildChildren(modelResource, child)); children.add(childResource); } } return children; } }