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.resources.app.workbench;
35  
36  import info.magnolia.resourceloader.Resource;
37  import info.magnolia.resourceloader.ResourceOrigin;
38  import info.magnolia.resourceloader.classpath.ClasspathResourceOrigin;
39  import info.magnolia.resourceloader.layered.LayeredResource;
40  import info.magnolia.ui.workbench.container.Refreshable;
41  
42  import java.util.ArrayList;
43  import java.util.Arrays;
44  import java.util.Collection;
45  import java.util.Collections;
46  import java.util.HashSet;
47  import java.util.Iterator;
48  import java.util.LinkedHashMap;
49  import java.util.LinkedList;
50  import java.util.List;
51  import java.util.Map;
52  import java.util.Set;
53  
54  import javax.annotation.Nullable;
55  
56  import org.apache.commons.lang3.ObjectUtils;
57  import org.apache.tika.Tika;
58  import org.slf4j.Logger;
59  import org.slf4j.LoggerFactory;
60  
61  import com.google.common.base.Function;
62  import com.google.common.base.Preconditions;
63  import com.google.common.base.Predicate;
64  import com.google.common.cache.CacheBuilder;
65  import com.google.common.cache.CacheLoader;
66  import com.google.common.cache.LoadingCache;
67  import com.google.common.collect.Collections2;
68  import com.google.common.collect.Iterables;
69  import com.google.common.collect.Lists;
70  import com.google.common.collect.Sets;
71  import com.vaadin.data.Collapsible;
72  import com.vaadin.data.Container;
73  import com.vaadin.data.Item;
74  import com.vaadin.data.Property;
75  import com.vaadin.data.util.AbstractContainer;
76  import com.vaadin.data.util.ObjectProperty;
77  import com.vaadin.data.util.PropertysetItem;
78  
79  /**
80   * Vaadin hierarchical {@link Container} implementation representing a resource {@link ResourceOrigin}.
81   * <p>
82   * Uses the {@link Resource} as itemId.
83   */
84  public class ResourcesContainer extends AbstractContainer implements Container, Container.Indexed, Container.Hierarchical, Collapsible, Refreshable, Container.ItemSetChangeNotifier {
85  
86      private static final Logger log = LoggerFactory.getLogger(ResourcesContainer.class);
87      private static final String UNSUPPORTED_WRITE_OPERATION = "ResourceContainer does not support write operations.";
88      private static final int DEFAULT_ITEM_CACHE_SIZE = 150;
89  
90      public static final Object RESOURCE_NAME = "name";
91      public static final String ORIGIN_NAME_PROPERTY_ID = "originName";
92      public static final String CONTENT_PROPERTY_ID = "content";
93      public static final String OVERRIDING_PROPERTY_ID = "overriding";
94      public static final String RESOURCE_PATH = "path";
95      public static final String FORMAT_PROPERTY_ID = "format";
96      public static final String DIRECTORY_PROPERTY_ID = "directory";
97  
98  
99      private final Map<String, Class<?>> containerPropertyIds = new LinkedHashMap<>();
100 
101     private final ResourceOrigin origin;
102     private final List<String> moduleNames;
103     private boolean classpathResourcesFiltered;
104 
105     private LinkedList<String> visibleIds = new LinkedList<>();
106     private Set<String> expandedIds = new HashSet<>();
107     private LoadingCache<String, Item> cache;
108 
109     public ResourcesContainer(ResourceOrigin origin, List<String> moduleNames) {
110         this.origin = origin;
111         this.moduleNames = moduleNames;
112         visibleIds.addAll(rootItemIds());
113         cache = initializeCache();
114     }
115 
116     /**
117      * Sets whether or not classpath resources should be tentatively filtered based on their registered magnolia modules.
118      *
119      * @param classpathResourcesFiltered setting this to {@code true} only shows classpath resources whose top-level directory is a registered magnolia module;
120      * setting this to {@code false} shows classpath resources from all classpathUrls (still pre-filtered in ClasspathResourceOrigin).
121      * @see info.magnolia.resourceloader.classpath.ClasspathResourceOrigin#excludedPackages()
122      */
123     public void setClasspathResourcesFiltered(boolean classpathResourcesFiltered) {
124         if (this.classpathResourcesFiltered != classpathResourcesFiltered) {
125             this.classpathResourcesFiltered = classpathResourcesFiltered;
126             showRootsOnly();
127         }
128     }
129 
130     // CONTAINER READ OPERATIONS
131 
132     @Override
133     public Item getItem(Object itemId) {
134         Preconditions.checkArgument(itemId instanceof String, "Expected String argument, but got %s", itemId == null ? "null" : itemId.getClass().getSimpleName());
135         final String resourcePath = (String) itemId;
136         return cache.getUnchecked(resourcePath);
137     }
138 
139     private Item createItem(String resourcePath) {
140         if (!origin.hasPath(resourcePath)) {
141             throw new IllegalArgumentException(String.format("Path [%s] does not exist", resourcePath));
142         }
143 
144         final Resource resource = origin.getByPath(resourcePath);
145         return ResourcesContainer.newItem(resource);
146     }
147 
148     // STATIC ITEM CREATION (temporary)
149     // TODO to be moved either to some kind of factory component, or to concrete Item implementation
150     public static Item newItem(Resource resource) {
151         final PropertysetItem item = new PropertysetItem();
152         // directories don't have a relevant origin
153         item.addItemProperty(RESOURCE_NAME, new ObjectProperty<>(resource.getName()));
154         item.addItemProperty(RESOURCE_PATH, new ObjectProperty<>(resource.getPath()));
155         item.addItemProperty(DIRECTORY_PROPERTY_ID, new ObjectProperty<>(resource.isDirectory()));
156         if (resource.isFile()) {
157             if (resource instanceof LayeredResource) {
158                 final LayeredResource layeredResource = (LayeredResource) resource;
159                 item.addItemProperty(ORIGIN_NAME_PROPERTY_ID, new ObjectProperty<>(layeredResource.getFirst().getOrigin().getName()));
160                 item.addItemProperty(CONTENT_PROPERTY_ID, new ResourceContentProperty(resource));
161                 item.addItemProperty(OVERRIDING_PROPERTY_ID, new ObjectProperty<>(isOverriding(layeredResource)));
162                 // TODO: This is sub-optimal. The resource type resolution should be a part of API! See https://jira.magnolia-cms.com/browse/MGNLRES-175
163                 item.addItemProperty(FORMAT_PROPERTY_ID, new ObjectProperty<>(new Tika().detect(resource.getName())));
164             } else {
165                 log.error("Unexpectedly encountered Resource {} of type {}, which was not an instance of LayeredResource.", resource, resource.getClass().getSimpleName());
166             }
167         }
168         return item;
169     }
170 
171     protected static boolean isOverriding(LayeredResource resource) {
172         return resource.getLayers().size() > 1;
173     }
174 
175     @Override
176     public Collection<String> getContainerPropertyIds() {
177         return Collections.unmodifiableCollection(containerPropertyIds.keySet());
178     }
179 
180     @Override
181     public Collection<String> getItemIds() {
182         return Collections.unmodifiableCollection(visibleIds);
183     }
184 
185     @Override
186     public Property<?> getContainerProperty(Object itemId, Object propertyId) {
187         return getItem(itemId).getItemProperty(propertyId);
188     }
189 
190     @Override
191     public boolean addContainerProperty(Object propertyId, Class<?> type, Object defaultValue) throws UnsupportedOperationException {
192         containerPropertyIds.put((String) propertyId, type);
193         return true;
194     }
195 
196     @Override
197     public boolean removeContainerProperty(Object propertyId) throws UnsupportedOperationException {
198         return containerPropertyIds.remove(propertyId) != null;
199     }
200 
201     @Override
202     public Class<?> getType(Object propertyId) {
203         return containerPropertyIds.get(propertyId);
204     }
205 
206     @Override
207     public int size() {
208         return visibleIds.size();
209     }
210 
211     @Override
212     public boolean containsId(Object itemId) {
213         return origin.hasPath(((String) itemId));
214     }
215 
216     // CONTAINER.ORDERED
217 
218     @Override
219     public Object nextItemId(Object itemId) {
220         int index = visibleIds.indexOf(itemId);
221         if (index == -1 || index == visibleIds.size() - 1) {
222             return null;
223         }
224         return visibleIds.get(index + 1);
225     }
226 
227     @Override
228     public Object prevItemId(Object itemId) {
229         int index = visibleIds.indexOf(itemId);
230         if (index <= 0) {
231             return null;
232         }
233         return visibleIds.get(index - 1);
234     }
235 
236     @Override
237     public Object firstItemId() {
238         return visibleIds.getFirst();
239     }
240 
241     @Override
242     public Object lastItemId() {
243         return visibleIds.getLast();
244     }
245 
246     @Override
247     public boolean isFirstId(Object itemId) {
248         return ObjectUtils.equals(itemId, visibleIds.getFirst());
249     }
250 
251     @Override
252     public boolean isLastId(Object itemId) {
253         return ObjectUtils.equals(itemId, visibleIds.getLast());
254     }
255 
256     // CONTAINER.INDEXED
257 
258     @Override
259     public int indexOfId(Object itemId) {
260         return visibleIds.indexOf(itemId);
261     }
262 
263     @Override
264     public Object getIdByIndex(int index) {
265         return visibleIds.get(index);
266     }
267 
268     @Override
269     public List<String> getItemIds(int startIndex, int numberOfItems) {
270         return visibleIds.subList(startIndex, Math.min(startIndex + numberOfItems, visibleIds.size()));
271     }
272 
273     // CONTAINER.HIERARCHICAL
274 
275     @Override
276     public List<String> getChildren(Object itemId) {
277         return Lists.transform(origin.getByPath((String) itemId).listChildren(), resourceToPath());
278     }
279 
280     @Override
281     public String getParent(Object itemId) {
282         if (itemId == null) {
283             throw new IllegalArgumentException("The provided itemId is null.");
284         }
285 
286         final Resource parent = origin.getByPath((String) itemId).getParent();
287         if (parent != null && ObjectUtils.notEqual(origin.getRoot(), parent)) {
288             return parent.getPath();
289         }
290 
291         return null;
292     }
293 
294     @Override
295     public Collection<String> rootItemIds() {
296         final Resource root = origin.getRoot();
297         final List<String> directRootChildren = Lists.transform(root.listChildren(), resourceToPath());
298 
299         if (!classpathResourcesFiltered) {
300             return directRootChildren;
301         }
302 
303         return Collections2.filter(directRootChildren, new Predicate<String>() {
304 
305             private boolean isClassPathResource(LayeredResource layeredResource) {
306                 final ResourceOrigin activeOrigin = layeredResource.getFirst().getOrigin();
307                 return activeOrigin instanceof ClasspathResourceOrigin;
308             }
309 
310             @Override
311             public boolean apply(String resourcePath) {
312                 final LayeredResource layeredResource = (LayeredResource) origin.getByPath(resourcePath);
313                 boolean isClasspathResource = isClassPathResource(layeredResource);
314                 return !isClasspathResource || moduleNames.contains(layeredResource.getName());
315             }
316         });
317     }
318 
319 
320     @Override
321     public boolean areChildrenAllowed(Object itemId) {
322         return origin.getByPath((String) itemId).isDirectory();
323     }
324 
325     @Override
326     public boolean isRoot(Object itemId) {
327         return origin.getRoot().equals(origin.getByPath((String) itemId).getParent());
328     }
329 
330     @Override
331     public boolean hasChildren(Object itemId) {
332         return !origin.getByPath((String) itemId).listChildren().isEmpty();
333     }
334 
335     // COLLAPSIBLE
336 
337     @Override
338     public void setCollapsed(Object itemId, boolean collapsed) {
339         if (!collapsed) {
340             doExpand(itemId);
341         } else {
342             doCollapse(itemId);
343         }
344     }
345 
346     protected void doExpand(Object itemId) {
347         final String pathToExpand = (String) itemId;
348 
349         // item is already expanded
350         if (expandedIds.contains(pathToExpand)) {
351             return;
352         }
353 
354         // Make sure all the parent nodes are also expanded
355         // (has to be done e.g. if node is expanded programmatically).
356         final String parentId = getParent(itemId);
357         if (parentId != null && !expandedIds.contains(parentId)) {
358             doExpand(parentId);
359         }
360 
361 
362         int insertionIndex = visibleIds.indexOf(itemId) + 1;
363         List<String> inserts = new ArrayList<>();
364 
365         Resource parentResource = origin.getByPath(pathToExpand);
366         inserts.addAll(Lists.transform(parentResource.listChildren(), resourceToPath()));
367 
368         int i = 0;
369         while (i < inserts.size()) {
370             String child = inserts.get(i);
371             i++;
372             if (expandedIds.contains(child)) {
373                 inserts.addAll(i, Lists.transform(origin.getByPath(child).listChildren(), resourceToPath()));
374             }
375         }
376         visibleIds.addAll(insertionIndex, inserts);
377         expandedIds.add(pathToExpand);
378     }
379 
380     protected void doCollapse(Object itemId) {
381         int fromIndex = visibleIds.indexOf(itemId) + 1;
382         // Item is the last in container, nothing to collapse
383         if (fromIndex == visibleIds.size()) {
384             return;
385         }
386 
387         int toIndex = fromIndex;
388         // Find first non-descendant of itemId
389         Iterator<String> it = visibleIds.subList(fromIndex, visibleIds.size()).iterator();
390         List<String> ancestors = new ArrayList<>(Arrays.asList((String) itemId));
391         while (it.hasNext()) {
392             String nextPath = it.next();
393             if (ancestors.contains(origin.getByPath(nextPath).getParent().getPath())) {
394                 toIndex++;
395                 ancestors.add(nextPath);
396             } else {
397                 break;
398             }
399         }
400         visibleIds.subList(fromIndex, toIndex).clear();
401         expandedIds.remove(itemId);
402     }
403 
404     @Override
405     public boolean isCollapsed(Object itemId) {
406         return !expandedIds.contains(itemId);
407     }
408 
409     protected void showRootsOnly() {
410         this.visibleIds.clear();
411 
412         this.visibleIds.addAll(rootItemIds());
413         this.expandedIds.clear();
414 
415         fireItemSetChange();
416     }
417 
418     // REFRESHABLE
419     @Override
420     public void refresh() {
421         final Predicate<String> originHasPath = new Predicate<String>() {
422             @Override
423             public boolean apply(@Nullable String input) {
424                 return origin.hasPath(input);
425             }
426         };
427 
428         this.visibleIds = Lists.newLinkedList(Iterables.filter(visibleIds, originHasPath));
429         this.expandedIds = Sets.newHashSet(Iterables.filter(expandedIds, originHasPath));
430         this.cache.invalidateAll();
431 
432         fireItemSetChange();
433     }
434 
435     // WRITE OPERATIONS
436 
437     @Override
438     public Item addItem(Object itemId) throws UnsupportedOperationException {
439         throw new UnsupportedOperationException(UNSUPPORTED_WRITE_OPERATION);
440     }
441 
442     @Override
443     public Object addItem() throws UnsupportedOperationException {
444         throw new UnsupportedOperationException(UNSUPPORTED_WRITE_OPERATION);
445     }
446 
447     @Override
448     public boolean removeItem(Object itemId) throws UnsupportedOperationException {
449         throw new UnsupportedOperationException(UNSUPPORTED_WRITE_OPERATION);
450     }
451 
452     @Override
453     public boolean removeAllItems() throws UnsupportedOperationException {
454         throw new UnsupportedOperationException(UNSUPPORTED_WRITE_OPERATION);
455     }
456 
457     @Override
458     public Object addItemAfter(Object previousItemId) throws UnsupportedOperationException {
459         throw new UnsupportedOperationException(UNSUPPORTED_WRITE_OPERATION);
460     }
461 
462     @Override
463     public Item addItemAfter(Object previousItemId, Object newItemId) throws UnsupportedOperationException {
464         throw new UnsupportedOperationException(UNSUPPORTED_WRITE_OPERATION);
465     }
466 
467     @Override
468     public Object addItemAt(int index) throws UnsupportedOperationException {
469         throw new UnsupportedOperationException(UNSUPPORTED_WRITE_OPERATION);
470     }
471 
472     @Override
473     public Item addItemAt(int index, Object newItemId) throws UnsupportedOperationException {
474         throw new UnsupportedOperationException(UNSUPPORTED_WRITE_OPERATION);
475     }
476 
477     @Override
478     public boolean setParent(Object itemId, Object newParentId) throws UnsupportedOperationException {
479         throw new UnsupportedOperationException(UNSUPPORTED_WRITE_OPERATION);
480     }
481 
482     @Override
483     public boolean setChildrenAllowed(Object itemId, boolean areChildrenAllowed) throws UnsupportedOperationException {
484         throw new UnsupportedOperationException(UNSUPPORTED_WRITE_OPERATION);
485     }
486 
487     // OVERRIDES to honor Container.ItemSetChangeNotifier's public interface (AbstractContainer defines those methods as protected)
488 
489     @Override
490     public void addItemSetChangeListener(ItemSetChangeListener listener) {
491         super.addItemSetChangeListener(listener);
492     }
493 
494     @Override
495     public void addListener(ItemSetChangeListener listener) {
496         super.addListener(listener);
497     }
498 
499     @Override
500     public void removeItemSetChangeListener(ItemSetChangeListener listener) {
501         super.removeItemSetChangeListener(listener);
502     }
503 
504     @Override
505     public void removeListener(ItemSetChangeListener listener) {
506         super.removeListener(listener);
507     }
508 
509     private Function<Resource, String> resourceToPath() {
510         return new Function<Resource, String>() {
511             @Nullable
512             @Override
513             public String apply(Resource resource) {
514                 return resource.getPath();
515             }
516         };
517     }
518 
519     private LoadingCache<String, Item> initializeCache() {
520         return CacheBuilder
521                 .newBuilder()
522                 .maximumSize(DEFAULT_ITEM_CACHE_SIZE)
523                 .build(new CacheLoader<String, Item>() {
524                     @Override
525                     public Item load(String itemId) throws Exception {
526                         return createItem(itemId);
527                     }
528                 });
529     }
530 }