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