View Javadoc
1   /**
2    * This file Copyright (c) 2015-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.resourceloader.layered;
35  
36  import static com.google.common.collect.Iterables.*;
37  import static info.magnolia.resourceloader.ResourceOriginChange.Type.*;
38  import static info.magnolia.resourceloader.ResourceOriginChange.resourceChange;
39  
40  import info.magnolia.resourceloader.AbstractResourceOrigin;
41  import info.magnolia.resourceloader.Resource;
42  import info.magnolia.resourceloader.ResourceChangeHandler;
43  import info.magnolia.resourceloader.ResourceChangeHandlerRegistration;
44  import info.magnolia.resourceloader.ResourceOrigin;
45  import info.magnolia.resourceloader.ResourceOriginChange;
46  import info.magnolia.resourceloader.ResourceOriginFactory;
47  import info.magnolia.resourceloader.util.Functions;
48  
49  import java.io.IOException;
50  import java.io.InputStream;
51  import java.io.Reader;
52  import java.nio.charset.Charset;
53  import java.util.ArrayList;
54  import java.util.Arrays;
55  import java.util.List;
56  
57  import org.slf4j.Logger;
58  import org.slf4j.LoggerFactory;
59  
60  import com.google.auto.factory.AutoFactory;
61  import com.google.common.base.Function;
62  import com.google.common.base.Optional;
63  import com.google.common.base.Predicate;
64  import com.google.common.collect.Iterables;
65  import com.google.common.collect.Lists;
66  import com.google.common.collect.Ordering;
67  
68  /**
69   * An implementation of {@link ResourceOrigin} which aggregates
70   * other instances of {@link ResourceOrigin}; resources located on the same path will
71   * be served from the foremost ResourceOrigin, but resources that exist only on one path will always be served.
72   */
73  @AutoFactory(implementing = ResourceOriginFactory.class)
74  public class LayeredResourceOrigin extends AbstractResourceOrigin<LayeredResource> {
75  
76      private static final Logger log = LoggerFactory.getLogger(LayeredResourceOrigin.class);
77  
78      private final List<ResourceOrigin> origins;
79  
80      // No injected arguments so far, which is fine. Just keeping a factory for consistency.
81      LayeredResourceOrigin(String name, ResourceOrigin... origins) {
82          super(name);
83          this.origins = Arrays.asList(origins);
84      }
85  
86      @Override
87      public LayeredResource getRoot() {
88          final List<Resource> roots = Lists.transform(origins, Functions.getRoot());
89          return newLayeredResource(roots);
90      }
91  
92      @Override
93      public ResourceChangeHandlerRegistration registerResourceChangeHandler(final ResourceChangeHandler changeHandler) {
94          final ResourceChangeHandler relayingChangeHandler = new RelayingChangeHandler(changeHandler);
95  
96          final List<ResourceChangeHandlerRegistration> registrations = Lists.newArrayListWithCapacity(origins.size() + 1);
97          registrations.add(super.registerResourceChangeHandler(relayingChangeHandler));
98  
99          for (final ResourceOrigin origin : origins) {
100             registrations.add(origin.registerResourceChangeHandler(relayingChangeHandler));
101         }
102 
103         return new AggregateChangeHandlerRegistration(registrations);
104     }
105 
106     @Override
107     public LayeredResource getByPath(String path) {
108         final Iterable<ResourceOrigin> matchingOrigins = filter(this.origins, Functions.hasPath(path));
109         final List<Resource> matchingResources = Lists.newArrayList(transform(matchingOrigins, Functions.getByPath(path)));
110         if (matchingResources.isEmpty()) {
111             throw new ResourceNotFoundException(this, path);
112         }
113         return newLayeredResource(matchingResources);
114     }
115 
116     @Override
117     public boolean hasPath(String path) {
118         return any(origins, Functions.hasPath(path));
119     }
120 
121     @Override
122     protected boolean isFile(LayeredResource resource) {
123         return resource.getFirst().isFile();
124     }
125 
126     @Override
127     protected boolean isDirectory(LayeredResource resource) {
128         return resource.getFirst().isDirectory();
129     }
130 
131     @Override
132     protected boolean isEditable(LayeredResource resource) {
133         return resource.getFirst().isEditable();
134     }
135 
136     @Override
137     protected String getPath(LayeredResource resource) {
138         return resource.getFirst().getPath();
139     }
140 
141     @Override
142     protected String getName(LayeredResource resource) {
143         return resource.getFirst().getName();
144     }
145 
146     @Override
147     protected long getLastModified(LayeredResource resource) {
148         return resource.getFirst().getLastModified();
149     }
150 
151     @Override
152     protected List<LayeredResource> listChildren(LayeredResource resource) {
153         List<LayeredResource> layeredResources = new ArrayList<>();
154 
155         // expecting layers to be correctly ordered
156         final List<Resource> layers = resource.getLayers();
157         for (Resource layer : layers) {
158             List<Resource> layerChildren = layer.listChildren();
159 
160             // optimizing first round, just transform whole first layer into layered resources
161             if (layeredResources.isEmpty()) {
162                 layeredResources.addAll(Lists.transform(layerChildren, new Function<Resource, LayeredResource>() {
163                     @Override
164                     public LayeredResource apply(Resource input) {
165                         return new LayeredResource(LayeredResourceOrigin.this, input.getPath(), Lists.newArrayList(input));
166                     }
167                 }));
168                 continue;
169             }
170 
171             for (final Resource r : layerChildren) {
172                 Optional<LayeredResource> matchingResource = Iterables.tryFind(layeredResources, new Predicate<LayeredResource>() {
173                     @Override
174                     public boolean apply(LayeredResource input) {
175                         return r.getPath().equals(input.getPath()) && r.isFile() == input.isFile();
176                     }
177                 });
178                 if (matchingResource.isPresent()) {
179                     LayeredResource l = matchingResource.get();
180                     l.getLayers().add(r);
181                 } else {
182                     LayeredResource l = new LayeredResource(this, r.getPath(), Lists.newArrayList(r));
183                     layeredResources.add(l);
184                 }
185             }
186         }
187         return Ordering.from(Functions.compareByHandle()).immutableSortedCopy(layeredResources);
188     }
189 
190     @Override
191     protected LayeredResource getParent(LayeredResource resource) {
192         // This would only get the parents from the layers in the given resource, but that parent might exist in other resources too
193         // final List<Resource> parents = Lists.transform(resource.getLayers(), Functions.getParent());
194 
195         // So instead, we pick the parent of the first layer, and do a getByPath, which feels a bit underwhelming.
196         Resource parent = resource.getFirst().getParent();
197         return parent != null ? getByPath(parent.getPath()) : null;
198     }
199 
200     @Override
201     protected InputStream doOpenStream(LayeredResource resource) throws IOException {
202         return resource.getFirst().openStream();
203     }
204 
205     @Override
206     protected Reader openReader(LayeredResource resource) throws IOException {
207         return resource.getFirst().openReader();
208     }
209 
210     /**
211      * Overridden to throw an exception; since we override {@link #openReader(LayeredResource)},
212      * this method used in {@link AbstractResourceOrigin#openReader(info.magnolia.resourceloader.AbstractResource)}
213      * should never be called.
214      */
215     @Override
216     protected Charset getCharsetFor(LayeredResource resource) {
217         throw new IllegalStateException("This method should not be called");
218     }
219 
220     protected LayeredResource newLayeredResource(List<Resource> resources) {
221         // Sanity checks -- are all the resources on the same path and are they all dir/files ?
222         final String path = resources.get(0).getPath();
223         final boolean isDirectory = resources.get(0).isDirectory();
224         if (!all(resources, Functions.pathEquals(path))) {
225             throw new IllegalStateException("Given resources don't match path [" + path + "]: " + resources);
226         }
227         final Predicate<Resource> dirOrFilePredicate = isDirectory ? Functions.isDirectory() : Functions.isFile();
228         if (!all(resources, dirOrFilePredicate)) {
229             log.warn("Resources at {} are not all directory/file: {}", path, resources);
230             resources = Lists.newArrayList(Iterables.filter(resources, dirOrFilePredicate));
231         }
232         return new LayeredResource(this, path, resources);
233     }
234 
235     /**
236      * Gathers all the {@link ResourceChangeHandlerRegistration handler registrations} from all the layers related to the same
237      * {@link RelayingChangeHandler}.
238      */
239     private static class AggregateChangeHandlerRegistration implements ResourceChangeHandlerRegistration {
240 
241         private final List<ResourceChangeHandlerRegistration> registrations;
242 
243         public AggregateChangeHandlerRegistration(List<ResourceChangeHandlerRegistration> registrations) {
244             this.registrations = registrations;
245         }
246 
247         @Override
248         public void unRegister() {
249             for (final ResourceChangeHandlerRegistration registration : registrations) {
250                 registration.unRegister();
251             }
252 
253         }
254     }
255 
256     /**
257      * Wraps another {@link ResourceChangeHandler resource change handler}, gets the changes from the 'layer' {@link ResourceOrigin resource origins} and
258      * relays those to the wrapped handler in a modified form:
259      * <ul>
260      * <li>Original change's {@link ResourceOrigin origin} is replaced with the current {@link LayeredResourceOrigin};</li>
261      * <li>{@link ResourceOriginChange Resource changes} not related to the currently used 'layer' are not communicated at all (ignored);</li>
262      * <li>If a currently utilised 'layer' is deleted revealing some other layer under it - then the deletion is treated as a modification.</li>
263      * </ul>
264      */
265     private class RelayingChangeHandler implements ResourceChangeHandler {
266 
267         private final ResourceChangeHandler delegate;
268 
269         RelayingChangeHandler(ResourceChangeHandler delegate) {
270             this.delegate = delegate;
271         }
272 
273         @Override
274         public void onResourceChanged(ResourceOriginChange change) {
275             final Optional<LayeredResource> resource = hasPath(change.getRelatedResourcePath()) ? Optional.of(getByPath(change.getRelatedResourcePath())) : Optional.<LayeredResource>absent();
276             final ResourceOriginChange.Builder relayedChange =
277                     resourceChange().
278                         inOrigin(LayeredResourceOrigin.this).
279                         at(change.getRelatedResourcePath());
280 
281             switch (change.getType()) {
282             case MODIFIED:
283                 // Dispatch resource modification iff it is actively used at the moment - otherwise ignore the change
284                 if (resource.isPresent() && resource.get().getFirst().getOrigin().equals(change.getRelatedOrigin())) {
285                     delegate.onResourceChanged(relayedChange.ofType(MODIFIED).build());
286                 }
287                 break;
288             case ADDED:
289                 // Dispatch resource addition iff it is actively used at the moment - otherwise ignore the change
290                 if (resource.isPresent() && resource.get().getFirst().getOrigin().equals(change.getRelatedOrigin())) {
291                     delegate.onResourceChanged(relayedChange.ofType(ADDED).build());
292                 }
293                 break;
294             case REMOVED:
295                 // if resource isn't present in any layer anymore - report it as deleted
296                 if (!resource.isPresent()) {
297                     delegate.onResourceChanged(relayedChange.ofType(REMOVED).build());
298                 } else {
299                     // resource is still present in one of the layers (e.g. hotfix on a file system resource was deleted) - treat that as modification
300                     delegate.onResourceChanged(relayedChange.ofType(MODIFIED).build());
301                 }
302                 break;
303             }
304         }
305     }
306 }