View Javadoc
1   /**
2    * This file Copyright (c) 2014-2016 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.config.source.yaml;
35  
36  import static info.magnolia.resourceloader.util.Functions.pathMatches;
37  import static info.magnolia.resourceloader.util.PredicatedResourceVisitor.onAllMatchingFiles;
38  
39  import info.magnolia.config.registry.DefinitionMetadata;
40  import info.magnolia.config.registry.DefinitionMetadataBuilder;
41  import info.magnolia.config.registry.DefinitionProvider;
42  import info.magnolia.config.registry.Registry;
43  import info.magnolia.config.registry.RegistryTypeNameUtil;
44  import info.magnolia.config.source.ConfigurationSource;
45  import info.magnolia.config.source.ConfigurationSourceType;
46  import info.magnolia.config.source.ConfigurationSourceTypes;
47  import info.magnolia.config.source.yaml.decoration.FileDefinitionDecorator;
48  import info.magnolia.config.source.yaml.decoration.FileDefinitionDecoratorResolver;
49  import info.magnolia.resourceloader.Resource;
50  import info.magnolia.resourceloader.ResourceChangeHandler;
51  import info.magnolia.resourceloader.ResourceOrigin;
52  import info.magnolia.resourceloader.ResourceOriginChange;
53  import info.magnolia.resourceloader.ResourceVisitor;
54  import info.magnolia.resourceloader.util.PredicatedResourceVisitor;
55  import info.magnolia.resourceloader.util.VoidFunction;
56  
57  import java.util.Collection;
58  import java.util.Collections;
59  import java.util.Iterator;
60  import java.util.Map;
61  import java.util.Map.Entry;
62  import java.util.Set;
63  import java.util.regex.Pattern;
64  
65  import org.slf4j.Logger;
66  import org.slf4j.LoggerFactory;
67  
68  import com.google.common.base.Optional;
69  import com.google.common.base.Predicate;
70  import com.google.common.collect.Collections2;
71  import com.google.common.collect.Maps;
72  import com.google.common.collect.Sets;
73  
74  /**
75   * Configuration source for any text-resource based configuration files.
76   *
77   * @param <T> type the source will provide
78   */
79  public abstract class AbstractFileResourceConfigurationSource<T> implements ConfigurationSource {
80  
81      private static final Logger log = LoggerFactory.getLogger(AbstractFileResourceConfigurationSource.class);
82  
83      // The following template helps to resolve the sub-resources of a folder (parametrised with '%s'):
84      // - the path must either strictly match the (folder itself),
85      // - or start with folder path followed by a slash (sub-folder or a file)
86      private static final String PATH_PREFIX_TEMPLATE = "^%s($|/.+)";
87  
88      private final ResourceOrigin<?> origin;
89      private final Pattern pathPattern;
90      private final Registry<T> registry;
91      private final PathToMetadataInferrer pathToMetadataInferrer;
92      private final Set<FileDefinitionDecoratorResolver> definitionDecoratorResolvers = Sets.newHashSet();
93      private final Map<String, FileDefinitionDecorator<T>> resolvedDefinitionDecorators = Maps.newHashMap();
94  
95      /**
96       * @param pathPattern a regular expression pattern used to determine whether a file should be considered by the source or not. If the pattern contains a group, it will be used to determine the name of the given object, if not explicitly configured.
97       */
98      public AbstractFileResourceConfigurationSource(ResourceOrigin<?> origin, Registry<T> registry, Pattern pathPattern) {
99          this.origin = origin;
100         this.registry = registry;
101         this.pathPattern = pathPattern;
102         this.pathToMetadataInferrer = new RegexBasedPathToMetadataInferrer(this.pathPattern);
103     }
104 
105     @Override
106     public ConfigurationSourceType type() {
107         return ConfigurationSourceTypes.file;
108     }
109 
110     @Override
111     public void start() {
112         log.info("Setting up {} to load {} definitions from resources", getClass().getSimpleName(), getRootType().getSimpleName());
113         final LoadAndRegisterFunction loadAndRegisterFunction = new LoadAndRegisterFunction();
114         final ResourceVisitor definitionProviderCollector = onAllMatchingFiles(pathMatches(pathPattern), loadAndRegisterFunction);
115 
116         origin.traverseWith(definitionProviderCollector);
117         origin.registerResourceChangeHandler(new ResourceChangeHandler() {
118             @Override
119             public void onResourceChanged(ResourceOriginChange change) {
120                 switch (change.getType()) {
121                 case MODIFIED:
122                 case ADDED:
123                     /**
124                      * If a resource matching this source's pattern has been added or modified - re-register
125                      * corresponding definition.
126                      */
127                     if (pathPattern.matcher(change.getRelatedResourcePath()).matches()) {
128                         loadAndRegister(origin.getByPath(change.getRelatedResourcePath()));
129                     }
130                     break;
131                 case REMOVED:
132                     /**
133                      * If a resource (maybe a folder) has been deleted - remove all the definitions
134                      * whose location matches the removed resource's path.
135                      */
136                     removeDefinitionsMatchingPath(change.getRelatedResourcePath());
137                     break;
138                 }
139             }
140         });
141 
142         startDecoration();
143     }
144 
145     protected void startDecoration() {
146         final VoidFunction<Resource> definitionDecoratorResolutionFunction = new DefinitionDecoratorResolutionFunction();
147         final ResourceVisitor definitionDecoratorResolver = PredicatedResourceVisitor.with(definitionDecoratorResolutionFunction);
148 
149         origin.traverseWith(definitionDecoratorResolver);
150         origin.registerResourceChangeHandler(new ResourceChangeHandler() {
151             @Override
152             public void onResourceChanged(ResourceOriginChange change) {
153                 switch (change.getType()) {
154                 case MODIFIED:
155                 case ADDED:
156                     final Resource resource = origin.getByPath(change.getRelatedResourcePath());
157                     definitionDecoratorResolutionFunction.apply(resource);
158                     break;
159                 case REMOVED:
160                     removeDefinitionDecoratorsMatchingPath(change.getRelatedResourcePath());
161                     break;
162                 }
163             }
164         });
165     }
166 
167     private void removeDefinitionDecoratorsMatchingPath(String removedResourcePath) {
168         // The following pattern would either match full file path, or partial path in case the removed path is a directory
169         final Pattern removedPathPattern = Pattern.compile(String.format(PATH_PREFIX_TEMPLATE, removedResourcePath));
170 
171         final Iterator<Entry<String,FileDefinitionDecorator<T>>> it = resolvedDefinitionDecorators.entrySet().iterator();
172         while (it.hasNext()) {
173             final Entry<String, FileDefinitionDecorator<T>> decoratorMapping = it.next();
174             if (removedPathPattern.matcher(decoratorMapping.getKey()).matches()) {
175                 registry.removeDecorator(decoratorMapping.getValue());
176                 it.remove();
177             }
178         }
179     }
180 
181     protected final void removeDefinitionsMatchingPath(String removedResourcePath) {
182         // The following pattern would either match full file path, or partial path in case the removed path is a directory
183         final Pattern removedPathPattern = Pattern.compile(String.format(PATH_PREFIX_TEMPLATE, removedResourcePath));
184         /**
185          * Here we filter all the metadata in the registry from all the configuration sources with the hope that
186          * no other source (like JCR) could produce a file resource-like definition location that would false-positively match
187          * the removed resource path. This is pretty unlikely to happen since all JCR paths start with {@code /modules/..}, but
188          * what about the future sources like code snippets?
189          *
190          * TODO - add config source id to definition metadata?
191          */
192         final Collection<DefinitionMetadata> idsToRemove = Collections2.filter(getRegistry().getAllMetadata(), new Predicate<DefinitionMetadata>() {
193             @Override
194             public boolean apply(DefinitionMetadata definitionMetadata) {
195                 return removedPathPattern.matcher(definitionMetadata.getLocation()).matches();
196             }
197         });
198 
199         if (!idsToRemove.isEmpty()) {
200             getRegistry().unregisterAndRegister(idsToRemove, Collections.<DefinitionProvider<T>>emptySet());
201         }
202     }
203 
204     public abstract void loadAndRegister(Resource resource);
205 
206     protected Registry<T> getRegistry() {
207         return registry;
208     }
209 
210     protected final Class<T> getRootType() {
211         return registry.type().baseClass();
212     }
213 
214     protected DefinitionMetadataBuilder createMetadata(Resource resource) {
215         final DefinitionMetadataBuilder metadataBuilder = registry.newMetadataBuilder()
216                 .type(getRegistry().type())
217                 .location(resource.getPath());
218         return pathToMetadataInferrer.populateFrom(metadataBuilder, resource);
219     }
220 
221 
222     protected final void registerDefinitionDecoratorResolver(FileDefinitionDecoratorResolver resolver) {
223         definitionDecoratorResolvers.add(resolver);
224     }
225 
226     /**
227      * Get currently resolved file decorators mapped to the paths of the files they are resolved from.
228      */
229     protected final Map<String, FileDefinitionDecorator<T>> getResolvedDefinitionDecorators() {
230         return resolvedDefinitionDecorators;
231     }
232 
233     private <T> Optional<FileDefinitionDecorator<T>> resolveDecorator(Resource resource) {
234         for (final FileDefinitionDecoratorResolver resolver : definitionDecoratorResolvers) {
235             final Optional<FileDefinitionDecorator<T>> resolutionResult = resolver.resolve(resource);
236             if (resolutionResult.isPresent()) {
237                 return resolutionResult;
238             }
239         }
240 
241         // Remove a previously resolved decorator corresponding to the path
242         // since it is not resolvable any longer
243         resolvedDefinitionDecorators.remove(resource.getPath());
244 
245         return Optional.absent();
246     }
247 
248     private class LoadAndRegisterFunction extends VoidFunction<Resource> {
249         @Override
250         public void doWith(Resource resource) {
251             log.debug("Loading {} ({})", resource, resource.getPath());
252             loadAndRegister(resource);
253         }
254     }
255 
256     private class DefinitionDecoratorResolutionFunction extends VoidFunction<Resource> {
257         @Override
258         public void doWith(Resource resource) {
259             final Optional<FileDefinitionDecorator<T>> resolvedDecorator = resolveDecorator(resource);
260             if (resolvedDecorator.isPresent()) {
261                 final FileDefinitionDecorator<T> decorator = resolvedDecorator.get();
262                 log.info("File based definition decorator resolved from [{}] will be (re-)added to [{}] registry", resource.getPath(), RegistryTypeNameUtil.pluralTypeNameOf(registry));
263                 resolvedDefinitionDecorators.put(resource.getPath(), decorator);
264                 registry.addDecorator(decorator);
265             }
266         }
267     }
268 }