View Javadoc
1   /**
2    * This file Copyright (c) 2014-2018 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 java.util.Comparator.comparing;
38  import static java.util.stream.Collectors.toList;
39  
40  import info.magnolia.config.registry.DefinitionMetadata;
41  import info.magnolia.config.registry.DefinitionMetadataBuilder;
42  import info.magnolia.config.registry.DefinitionProvider;
43  import info.magnolia.config.registry.Registry;
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.module.ModuleRegistry;
50  import info.magnolia.module.model.ModuleDefinition;
51  import info.magnolia.objectfactory.Components;
52  import info.magnolia.resourceloader.Resource;
53  import info.magnolia.resourceloader.ResourceOrigin;
54  import info.magnolia.resourceloader.ResourceVisitor;
55  import info.magnolia.resourceloader.util.FileResourceCollectorVisitor;
56  import info.magnolia.resourceloader.util.PredicatedResourceVisitor;
57  import info.magnolia.resourceloader.util.VoidFunction;
58  
59  import java.util.Collection;
60  import java.util.Collections;
61  import java.util.Iterator;
62  import java.util.List;
63  import java.util.Map;
64  import java.util.Map.Entry;
65  import java.util.Set;
66  import java.util.regex.Pattern;
67  
68  import org.slf4j.Logger;
69  import org.slf4j.LoggerFactory;
70  
71  import com.google.common.base.Optional;
72  import com.google.common.base.Predicate;
73  import com.google.common.collect.Collections2;
74  import com.google.common.collect.Maps;
75  import com.google.common.collect.Multimap;
76  import com.google.common.collect.MultimapBuilder;
77  import com.google.common.collect.Sets;
78  
79  /**
80   * Configuration source for any text-resource based configuration files.
81   *
82   * @param <T> type the source will provide
83   */
84  public abstract class AbstractFileResourceConfigurationSource<T> implements ConfigurationSource {
85  
86      private static final Logger log = LoggerFactory.getLogger(AbstractFileResourceConfigurationSource.class);
87  
88      // The following template helps to resolve the sub-resources of a folder (parametrised with '%s'):
89      // - the path must either strictly match the (folder itself),
90      // - or start with folder path followed by a slash (sub-folder or a file)
91      private static final String PATH_PREFIX_TEMPLATE = "^%s($|/.+)";
92  
93      private final ResourceOrigin<?> origin;
94      private final Pattern pathPattern;
95      private final ModuleRegistry moduleRegistry;
96      private final Registry<T> registry;
97      private final PathToMetadataInferrer pathToMetadataInferrer;
98      private final Set<FileDefinitionDecoratorResolver> definitionDecoratorResolvers = Sets.newHashSet();
99      private final Map<String, FileDefinitionDecorator<T>> resolvedDefinitionDecorators = Maps.newHashMap();
100 
101     /**
102      * @deprecated since 5.5.6 - use {@link #AbstractFileResourceConfigurationSource(ResourceOrigin, Registry, Pattern, ModuleRegistry)} instead.
103      */
104     @Deprecated
105     public AbstractFileResourceConfigurationSource(ResourceOrigin<?> origin, Registry<T> registry, Pattern pathPattern) {
106         this(origin, registry, pathPattern, Components.getComponent(ModuleRegistry.class));
107     }
108 
109     /**
110      * @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.
111      */
112     public AbstractFileResourceConfigurationSource(ResourceOrigin<?> origin, Registry<T> registry, Pattern pathPattern, ModuleRegistry moduleRegistry) {
113         this.origin = origin;
114         this.registry = registry;
115         this.pathPattern = pathPattern;
116         this.moduleRegistry = moduleRegistry;
117         this.pathToMetadataInferrer = new RegexBasedPathToMetadataInferrer(this.pathPattern);
118     }
119 
120     @Override
121     public ConfigurationSourceType type() {
122         return ConfigurationSourceTypes.file;
123     }
124 
125     @Override
126     public void start() {
127         log.info("Setting up {} to load {} definitions from resources", getClass().getSimpleName(), getRootType().getSimpleName());
128         final LoadAndRegisterFunction loadAndRegisterFunction = new LoadAndRegisterFunction();
129 
130 
131         final Multimap<String, Resource> definitionResources = MultimapBuilder.SetMultimapBuilder
132                 .hashKeys()
133                 .arrayListValues()
134                 .build();
135         final DefinitionMetadataBuilder metadataBuilder = registry.newMetadataBuilder();
136         final FileResourceCollectorVisitor definitionProviderCollector = FileResourceCollectorVisitor.on(pathMatches(pathPattern));
137 
138         origin.traverseWith(definitionProviderCollector);
139 
140         definitionProviderCollector.getCollectedResources().forEach(resource -> {
141             pathToMetadataInferrer.populateFrom(metadataBuilder, resource);
142             definitionResources.put(metadataBuilder.getModule(), resource);
143         });
144 
145         final List<String> moduleNameOrder = moduleRegistry
146                 .getModuleDefinitions().stream()
147                 .map(ModuleDefinition::getName)
148                 .collect(toList());
149 
150         definitionResources.entries().stream()
151                 .sorted(comparing(r -> moduleNameOrder.indexOf(r.getKey())))
152                 .map(Entry::getValue)
153                 .forEach(loadAndRegisterFunction::apply);
154 
155         origin.registerResourceChangeHandler(change -> {
156             switch (change.getType()) {
157             case MODIFIED:
158             case ADDED:
159                 /*
160                  * If a resource matching this source's pattern has been added or modified - re-register
161                  * corresponding definition.
162                  */
163                 if (pathPattern.matcher(change.getRelatedResourcePath()).matches()) {
164                     loadAndRegister(origin.getByPath(change.getRelatedResourcePath()));
165                 }
166                 break;
167             case REMOVED:
168                 /*
169                  * If a resource (maybe a folder) has been deleted - remove all the definitions
170                  * whose location matches the removed resource's path.
171                  */
172                 removeDefinitionsMatchingPath(change.getRelatedResourcePath());
173                 break;
174             }
175         });
176 
177         startDecoration();
178     }
179 
180     protected void startDecoration() {
181         final VoidFunction<Resource> definitionDecoratorResolutionFunction = new DefinitionDecoratorResolutionFunction();
182         final ResourceVisitor definitionDecoratorResolver = PredicatedResourceVisitor.with(definitionDecoratorResolutionFunction);
183 
184         origin.traverseWith(definitionDecoratorResolver);
185         origin.registerResourceChangeHandler(change -> {
186             switch (change.getType()) {
187             case MODIFIED:
188             case ADDED:
189                 final Resource resource = origin.getByPath(change.getRelatedResourcePath());
190                 definitionDecoratorResolutionFunction.apply(resource);
191                 break;
192             case REMOVED:
193                 removeDefinitionDecoratorsMatchingPath(change.getRelatedResourcePath());
194                 break;
195             }
196         });
197     }
198 
199     private void removeDefinitionDecoratorsMatchingPath(String removedResourcePath) {
200         // The following pattern would either match full file path, or partial path in case the removed path is a directory
201         final Pattern removedPathPattern = Pattern.compile(String.format(PATH_PREFIX_TEMPLATE, removedResourcePath));
202 
203         final Iterator<Entry<String, FileDefinitionDecorator<T>>> it = resolvedDefinitionDecorators.entrySet().iterator();
204         while (it.hasNext()) {
205             final Entry<String, FileDefinitionDecorator<T>> decoratorMapping = it.next();
206             if (removedPathPattern.matcher(decoratorMapping.getKey()).matches()) {
207                 registry.removeDecorator(decoratorMapping.getValue());
208                 log.info("File based definition decorator removed from [{}] in [{}] registry", removedResourcePath, registry.type().getPluralName());
209                 it.remove();
210             }
211         }
212     }
213 
214     protected final void removeDefinitionsMatchingPath(String removedResourcePath) {
215         // The following pattern would either match full file path, or partial path in case the removed path is a directory
216         final Pattern removedPathPattern = Pattern.compile(String.format(PATH_PREFIX_TEMPLATE, removedResourcePath));
217         /*
218          * Here we filter all the metadata in the registry from all the configuration sources with the hope that
219          * no other source (like JCR) could produce a file resource-like definition location that would false-positively match
220          * the removed resource path. This is pretty unlikely to happen since all JCR paths start with {@code /modules/..}, but
221          * what about the future sources like code snippets?
222          *
223          * TODO - add config source id to definition metadata?
224          */
225         final Collection<DefinitionMetadata> idsToRemove = Collections2.filter(getRegistry().getAllMetadata(), new Predicate<DefinitionMetadata>() {
226             @Override
227             public boolean apply(DefinitionMetadata definitionMetadata) {
228                 return removedPathPattern.matcher(definitionMetadata.getLocation()).matches();
229             }
230         });
231 
232         if (!idsToRemove.isEmpty()) {
233             getRegistry().unregisterAndRegister(idsToRemove, Collections.<DefinitionProvider<T>>emptySet());
234         }
235     }
236 
237     public abstract void loadAndRegister(Resource resource);
238 
239     protected Registry<T> getRegistry() {
240         return registry;
241     }
242 
243     protected final Class<T> getRootType() {
244         return registry.type().baseClass();
245     }
246 
247     protected DefinitionMetadataBuilder createMetadata(Resource resource) {
248         final DefinitionMetadataBuilder metadataBuilder = registry.newMetadataBuilder()
249                 .type(getRegistry().type())
250                 .location(resource.getPath())
251                 .configurationSourceType(ConfigurationSourceTypes.file);
252         return pathToMetadataInferrer.populateFrom(metadataBuilder, resource);
253     }
254 
255 
256     protected final void registerDefinitionDecoratorResolver(FileDefinitionDecoratorResolver resolver) {
257         definitionDecoratorResolvers.add(resolver);
258     }
259 
260     /**
261      * Get currently resolved file decorators mapped to the paths of the files they are resolved from.
262      */
263     protected final Map<String, FileDefinitionDecorator<T>> getResolvedDefinitionDecorators() {
264         return resolvedDefinitionDecorators;
265     }
266 
267     private Optional<FileDefinitionDecorator<T>> resolveDecorator(Resource resource) {
268         for (final FileDefinitionDecoratorResolver resolver : definitionDecoratorResolvers) {
269             final Optional<FileDefinitionDecorator<T>> resolutionResult = resolver.resolve(resource);
270             if (resolutionResult.isPresent()) {
271                 return resolutionResult;
272             }
273         }
274 
275         // Remove a previously resolved decorator corresponding to the path
276         // since it is not resolvable any longer
277         resolvedDefinitionDecorators.remove(resource.getPath());
278 
279         return Optional.absent();
280     }
281 
282     private class LoadAndRegisterFunction extends VoidFunction<Resource> {
283         @Override
284         public void doWith(Resource resource) {
285             log.debug("Loading {} ({})", resource, resource.getPath());
286             loadAndRegister(resource);
287         }
288     }
289 
290     private class DefinitionDecoratorResolutionFunction extends VoidFunction<Resource> {
291         @Override
292         public void doWith(Resource resource) {
293             final Optional<FileDefinitionDecorator<T>> resolvedDecorator = resolveDecorator(resource);
294             if (resolvedDecorator.isPresent()) {
295                 final FileDefinitionDecorator<T> decorator = resolvedDecorator.get();
296                 if (resolvedDefinitionDecorators.containsKey(resource.getPath())) {
297                     log.info("File based definition decorator modified at [{}] and will be updated in [{}] registry", resource.getPath(), registry.type().getPluralName());
298                 } else {
299                     log.info("File based definition decorator resolved from [{}] and will be added to [{}] registry", resource.getPath(), registry.type().getPluralName());
300                 }
301                 resolvedDefinitionDecorators.put(resource.getPath(), decorator);
302                 registry.addDecorator(decorator);
303             }
304         }
305     }
306 }