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