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