View Javadoc
1   /**
2    * This file Copyright (c) 2015 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 com.google.common.collect.Lists.newArrayList;
38  
39  import info.magnolia.resourceloader.AbstractResourceOrigin;
40  import info.magnolia.resourceloader.Resource;
41  import info.magnolia.resourceloader.ResourceOrigin;
42  import info.magnolia.resourceloader.ResourceOriginFactory;
43  import info.magnolia.resourceloader.ResourceVisitor;
44  import info.magnolia.resourceloader.util.Functions;
45  
46  import java.io.IOException;
47  import java.io.InputStream;
48  import java.io.Reader;
49  import java.nio.charset.Charset;
50  import java.util.Arrays;
51  import java.util.Collection;
52  import java.util.List;
53  import java.util.SortedSet;
54  
55  import com.google.auto.factory.AutoFactory;
56  import com.google.common.base.Function;
57  import com.google.common.collect.ImmutableList;
58  import com.google.common.collect.ImmutableListMultimap;
59  import com.google.common.collect.ImmutableSortedSet;
60  import com.google.common.collect.Lists;
61  import com.google.common.collect.Maps;
62  import com.google.common.collect.Multimaps;
63  import com.google.common.collect.SetMultimap;
64  
65  /**
66   * An implementation of {@link ResourceOrigin} which aggregates
67   * other instances of {@link ResourceOrigin}; resources located on the same path will
68   * be served from the foremost ResourceOrigin, but resources that exist only on one path will always be served.
69   */
70  @AutoFactory(implementing = ResourceOriginFactory.class)
71  public class LayeredResourceOrigin extends AbstractResourceOrigin<LayeredResource> {
72      private final List<ResourceOrigin> origins;
73  
74      // No injected arguments so far, which is fine. Just keeping a factory for consistency.
75      LayeredResourceOrigin(String name, ResourceOrigin... origins) {
76          super(name);
77          this.origins = Arrays.asList(origins);
78      }
79  
80      @Override
81      public LayeredResource getRoot() {
82          final List<Resource> roots = Lists.transform(origins, Functions.getRoot());
83          return newLayeredResource(roots);
84      }
85  
86      @Override
87      public void watchForChanges(ResourceVisitor visitor) {
88          for (ResourceOrigin origin : origins) {
89              origin.watchForChanges(new RelayerResourceVisitor(this, visitor));
90          }
91      }
92  
93      @Override
94      public LayeredResource getByPath(String path) {
95          final Iterable<ResourceOrigin> matchingOrigins = filter(this.origins, Functions.hasPath(path));
96          final List<Resource> matchingResources = newArrayList(transform(matchingOrigins, Functions.getByPath(path)));
97          if (matchingResources.isEmpty()) {
98              throw new ResourceNotFoundException(this, path);
99          }
100         return newLayeredResource(matchingResources);
101     }
102 
103     @Override
104     public boolean hasPath(String path) {
105         return any(origins, Functions.hasPath(path));
106     }
107 
108     @Override
109     protected boolean isFile(LayeredResource resource) {
110         return resource.getFirst().isFile();
111     }
112 
113     @Override
114     protected boolean isDirectory(LayeredResource resource) {
115         return resource.getFirst().isDirectory();
116     }
117 
118     @Override
119     protected boolean isEditable(LayeredResource resource) {
120         return resource.getFirst().isEditable();
121     }
122 
123     @Override
124     protected String getPath(LayeredResource resource) {
125         return resource.getFirst().getPath();
126     }
127 
128     @Override
129     protected String getName(LayeredResource resource) {
130         return resource.getFirst().getName();
131     }
132 
133     @Override
134     protected long getLastModified(LayeredResource resource) {
135         return resource.getFirst().getLastModified();
136     }
137 
138     @Override
139     protected List<LayeredResource> listChildren(LayeredResource resource) {
140         final Function<Resource, Collection<Resource>> delegatedFunction = new Function<Resource, Collection<Resource>>() {
141             @Override
142             public Collection<Resource> apply(Resource layer) {
143                 return layer.listChildren();
144             }
145         };
146         final List<Resource> layers = resource.getLayers();
147         return Lists.newArrayList(aggregateSet(layers, delegatedFunction));
148     }
149 
150     @Override
151     protected LayeredResource getParent(LayeredResource resource) {
152         // This would only get the parents from the layers in the given resource, but that parent might exist in other resources too
153         // final List<Resource> parents = Lists.transform(resource.getLayers(), Functions.getParent());
154 
155         // So instead, we pick the parent of the first layer, and do a getByPath, which feels a bit underwhelming.
156         Resource parent = resource.getFirst().getParent();
157         return parent != null ? getByPath(parent.getPath()) : null;
158     }
159 
160     @Override
161     protected InputStream doOpenStream(LayeredResource resource) throws IOException {
162         return resource.getFirst().openStream();
163     }
164 
165     @Override
166     protected Reader openReader(LayeredResource resource) throws IOException {
167         return resource.getFirst().openReader();
168     }
169 
170     /**
171      * Overridden to throw an exception; since we override {@link #openReader(LayeredResource)},
172      * this method used in {@link AbstractResourceOrigin#openReader(info.magnolia.resourceloader.AbstractResource)}
173      * should never be called.
174      */
175     @Override
176     protected Charset getCharsetFor(LayeredResource resource) {
177         throw new IllegalStateException("This method should not be called");
178     }
179 
180     /**
181      * @param <S> the type of "source" object on which the function is applied. ResourceOrigin or Resource.
182      */
183     protected <S> SortedSet<LayeredResource> aggregateSet(List<S> sources, Function<S, Collection<Resource>> delegatedFunction) {
184         // First, we build a map <String path, List<Resource> forEachOrigin>
185         final ImmutableListMultimap.Builder<String, Resource> builder = ImmutableListMultimap.builder();
186 
187         // for (ResourceOrigin source : origins) {
188         for (S source : sources) {
189             final Collection<Resource> result = delegatedFunction.apply(source);
190 
191             // this work but why not sanitize and use uniqueIndex to ensure we don't have twice the same path ? but then we can't pass to builder without further wrapping
192             // final ImmutableMultimap<String, Resource> map = Multimaps.index(paths, TO_PATH);
193             // TODO order of values must be add-order
194             final SetMultimap<String, Resource> map = Multimaps.forMap(Maps.uniqueIndex(result, Functions.getPath()));
195             builder.putAll(map);
196         }
197         final ImmutableListMultimap<String, Resource> map = builder.build();
198 
199         // Then we transform that map back to a Collection of LayeredResources
200         final ImmutableSortedSet.Builder<LayeredResource> listBuilder = ImmutableSortedSet.orderedBy(Functions.<LayeredResource>compareByHandle());
201 
202         for (String path : map.keySet()) {
203             final ImmutableList<Resource> resources = map.get(path);
204             listBuilder.add(newLayeredResource(resources));
205         }
206         return listBuilder.build();
207     }
208 
209     protected LayeredResource newLayeredResource(final List<Resource> resources) {
210         // Sanity checks -- are all the resources on the same path and are they all dir/files ?
211         final String path = resources.get(0).getPath();
212         final boolean isDirectory = resources.get(0).isDirectory();
213         if (!all(resources, Functions.pathEquals(path))) {
214             throw new IllegalStateException("Given resources don't match path [" + path + "]: " + resources);
215         }
216         if (!all(resources, isDirectory ? Functions.isDirectory() : Functions.isFile())) {
217             throw new IllegalStateException("Given resources are not all directory/file: " + resources);
218         }
219 
220         return new LayeredResource(this, path, resources);
221     }
222 
223 }