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.Map;
60  import java.util.Set;
61  import java.util.regex.Pattern;
62  
63  import org.slf4j.Logger;
64  import org.slf4j.LoggerFactory;
65  
66  import com.google.common.base.Optional;
67  import com.google.common.base.Predicate;
68  import com.google.common.collect.Collections2;
69  import com.google.common.collect.Maps;
70  import com.google.common.collect.Sets;
71  
72  /**
73   * Configuration source for any text-resource based configuration files.
74   *
75   * @param <T> type the source will provide
76   */
77  public abstract class AbstractFileResourceConfigurationSource<T> implements ConfigurationSource {
78  
79      private static final Logger log = LoggerFactory.getLogger(AbstractFileResourceConfigurationSource.class);
80  
81      private final ResourceOrigin origin;
82      private final Pattern pathPattern;
83      private final Registry<T> registry;
84      private final PathToMetadataInferrer pathToMetadataInferrer;
85      private final Set<FileDefinitionDecoratorResolver> definitionDecoratorResolvers = Sets.newHashSet();
86      private final Map<String, FileDefinitionDecorator<T>> resolvedDefinitionDecorators = Maps.newHashMap();
87  
88      /**
89       * @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.
90       */
91      public AbstractFileResourceConfigurationSource(ResourceOrigin origin, Registry<T> registry, Pattern pathPattern) {
92          this.origin = origin;
93          this.registry = registry;
94          this.pathPattern = pathPattern;
95          this.pathToMetadataInferrer = new RegexBasedPathToMetadataInferrer(this.pathPattern);
96      }
97  
98      @Override
99      public ConfigurationSourceType type() {
100         return ConfigurationSourceTypes.file;
101     }
102 
103     @Override
104     public void start() {
105         log.info("Setting up {} to load {} definitions from resources", getClass().getSimpleName(), getRootType().getSimpleName());
106         final LoadAndRegisterFunction loadAndRegisterFunction = new LoadAndRegisterFunction();
107         final ResourceVisitor definitionProviderCollector = onAllMatchingFiles(pathMatches(pathPattern), loadAndRegisterFunction);
108 
109         origin.traverseWith(definitionProviderCollector);
110         origin.registerResourceChangeHandler(new ResourceChangeHandler() {
111             @Override
112             public void onResourceChanged(ResourceOriginChange change) {
113                 switch (change.getType()) {
114                 case MODIFIED:
115                 case ADDED:
116                     /**
117                      * If a resource matching this source's pattern has been added or modified - re-register
118                      * corresponding definition.
119                      */
120                     if (pathPattern.matcher(change.getRelatedResourcePath()).matches()) {
121                         loadAndRegister(origin.getByPath(change.getRelatedResourcePath()));
122                     }
123                     break;
124                 case REMOVED:
125                     /**
126                      * If a resource (maybe a folder) has been deleted - remove all the definitions
127                      * whose location matches the removed resource's path.
128                      */
129                     removeDefinitionsMatchingPath(change.getRelatedResourcePath());
130                     break;
131                 }
132             }
133         });
134 
135         startDecoration();
136     }
137 
138     protected void startDecoration() {
139         final VoidFunction<Resource> definitionDecoratorResolutionFunction = new DefinitionDecoratorResolutionFunction();
140         final ResourceVisitor definitionDecoratorResolver = PredicatedResourceVisitor.with(definitionDecoratorResolutionFunction);
141 
142         origin.traverseWith(definitionDecoratorResolver);
143         origin.registerResourceChangeHandler(new ResourceChangeHandler() {
144             @Override
145             public void onResourceChanged(ResourceOriginChange change) {
146                 switch (change.getType()) {
147                 case MODIFIED:
148                 case ADDED:
149                     final Resource resource = origin.getByPath(change.getRelatedResourcePath());
150                     definitionDecoratorResolutionFunction.apply(resource);
151                     break;
152                 case REMOVED:
153                     final FileDefinitionDecorator<T> obsoleteDecorator = getResolvedDefinitionDecorators().get(change.getRelatedResourcePath());
154                     if (obsoleteDecorator != null) {
155                         resolvedDefinitionDecorators.remove(change.getRelatedResourcePath());
156                         registry.removeDecorator(obsoleteDecorator);
157                         return;
158                     }
159                     break;
160                 }
161             }
162         });
163     }
164 
165     protected final void removeDefinitionsMatchingPath(String removedResourcePath) {
166         // The following pattern would either match full file path, or partial path in case the removed path is a directory
167         final Pattern removedPathPattern = Pattern.compile(String.format("^%s(&|/.+)", removedResourcePath));
168         /**
169          * Here we filter all the metadata in the registry from all the configuration sources with the hope that
170          * no other source (like JCR) could produce a file resource-like definition location that would false-positively match
171          * the removed resource path. This is pretty unlikely to happen since all JCR paths start with {@code /modules/..}, but
172          * what about the future sources like code snippets?
173          *
174          * TODO - add config source id to definition metadata?
175          */
176         final Collection<DefinitionMetadata> idsToRemove = Collections2.filter(getRegistry().getAllMetadata(), new Predicate<DefinitionMetadata>() {
177             @Override
178             public boolean apply(DefinitionMetadata definitionMetadata) {
179                 return removedPathPattern.matcher(definitionMetadata.getLocation()).matches();
180             }
181         });
182 
183         if (!idsToRemove.isEmpty()) {
184             getRegistry().unregisterAndRegister(idsToRemove, Collections.<DefinitionProvider<T>>emptySet());
185         }
186     }
187 
188     public abstract void loadAndRegister(Resource resource);
189 
190     protected Registry<T> getRegistry() {
191         return registry;
192     }
193 
194     protected final Class<T> getRootType() {
195         return registry.type().baseClass();
196     }
197 
198     protected DefinitionMetadataBuilder createMetadata(Resource resource) {
199         final DefinitionMetadataBuilder metadataBuilder = registry.newMetadataBuilder()
200                 .type(getRegistry().type())
201                 .location(resource.getPath());
202         return pathToMetadataInferrer.populateFrom(metadataBuilder, resource);
203     }
204 
205 
206     protected final void registerDefinitionDecoratorResolver(FileDefinitionDecoratorResolver resolver) {
207         definitionDecoratorResolvers.add(resolver);
208     }
209 
210     /**
211      * Get currently resolved file decorators mapped to the paths of the files they are resolved from.
212      */
213     protected final Map<String, FileDefinitionDecorator<T>> getResolvedDefinitionDecorators() {
214         return resolvedDefinitionDecorators;
215     }
216 
217     private <T> Optional<FileDefinitionDecorator<T>> resolveDecorator(Resource resource) {
218         for (final FileDefinitionDecoratorResolver resolver : definitionDecoratorResolvers) {
219             final Optional<FileDefinitionDecorator<T>> resolutionResult = resolver.resolve(resource);
220             if (resolutionResult.isPresent()) {
221                 return resolutionResult;
222             }
223         }
224 
225         // Remove a previously resolved decorator corresponding to the path
226         // since it is not resolvable any longer
227         resolvedDefinitionDecorators.remove(resource.getPath());
228 
229         return Optional.absent();
230     }
231 
232     private class LoadAndRegisterFunction extends VoidFunction<Resource> {
233         @Override
234         public void doWith(Resource resource) {
235             log.debug("Loading {} ({})", resource, resource.getPath());
236             loadAndRegister(resource);
237         }
238     }
239 
240     private class DefinitionDecoratorResolutionFunction extends VoidFunction<Resource> {
241         @Override
242         public void doWith(Resource resource) {
243             final Optional<FileDefinitionDecorator<T>> resolvedDecorator = resolveDecorator(resource);
244             if (resolvedDecorator.isPresent()) {
245                 final FileDefinitionDecorator<T> decorator = resolvedDecorator.get();
246                 log.info("File based definition decorator resolved from [{}] will be (re-)added to [{}] registry", resource.getPath(), RegistryTypeNameUtil.pluralTypeNameOf(registry));
247                 resolvedDefinitionDecorators.put(resource.getPath(), decorator);
248                 registry.addDecorator(decorator);
249             }
250         }
251     }
252 }