View Javadoc
1   /**
2    * This file Copyright (c) 2015-2016 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         try {
293             if (itemId instanceof String) {
294                 Resource resource = origin.getByPath((String) itemId);
295                 final Resource parent = resource.getParent();
296                 if (parent != null && ObjectUtils.notEqual(origin.getRoot(), parent)) {
297                     return parent.getPath();
298                 }
299             }
300         } catch (ResourceOrigin.ResourceNotFoundException e) {
301             log.debug("Resource could not be found for path {}", itemId);
302         }
303 
304         return null;
305     }
306 
307     @Override
308     public Collection<String> rootItemIds() {
309         final Resource root = origin.getRoot();
310         final List<String> directRootChildren = Lists.transform(root.listChildren(), resourceToPath());
311 
312         if (!classpathResourcesFiltered) {
313             return directRootChildren;
314         }
315 
316         return Collections2.filter(directRootChildren, new Predicate<String>() {
317 
318             private boolean isClassPathResource(LayeredResource layeredResource) {
319                 final ResourceOrigin activeOrigin = layeredResource.getFirst().getOrigin();
320                 return activeOrigin instanceof ClasspathResourceOrigin;
321             }
322 
323             @Override
324             public boolean apply(String resourcePath) {
325                 final LayeredResource layeredResource = (LayeredResource) origin.getByPath(resourcePath);
326                 boolean isClasspathResource = isClassPathResource(layeredResource);
327                 return !isClasspathResource || moduleNames.contains(layeredResource.getName());
328             }
329         });
330     }
331 
332 
333     @Override
334     public boolean areChildrenAllowed(Object itemId) {
335         return origin.getByPath((String) itemId).isDirectory();
336     }
337 
338     @Override
339     public boolean isRoot(Object itemId) {
340         try {
341             if (itemId instanceof String) {
342                 Resource resource = origin.getByPath((String) itemId);
343                 return origin.getRoot().equals(resource.getParent());
344             }
345         } catch (ResourceOrigin.ResourceNotFoundException e) {
346             log.debug("Resource could not be found for path {}", itemId);
347         }
348 
349         return false;
350     }
351 
352     @Override
353     public boolean hasChildren(Object itemId) {
354         return !origin.getByPath((String) itemId).listChildren().isEmpty();
355     }
356 
357     // COLLAPSIBLE
358 
359     @Override
360     public void setCollapsed(Object itemId, boolean collapsed) {
361         try {
362             if (!collapsed) {
363                 doExpand(itemId);
364             } else {
365                 doCollapse(itemId);
366             }
367         } catch (ResourceOrigin.ResourceNotFoundException e) {
368             log.info("Resource {} is removed or deleted, container needs to be refreshed!", itemId);
369             refresh();
370         }
371     }
372 
373     protected void doExpand(Object itemId) {
374         final String pathToExpand = (String) itemId;
375 
376         // item is already expanded
377         if (expandedIds.contains(pathToExpand)) {
378             return;
379         }
380 
381         // Make sure all the parent nodes are also expanded
382         // (has to be done e.g. if node is expanded programmatically).
383         final String parentId = getParent(itemId);
384         if (parentId != null && !expandedIds.contains(parentId)) {
385             doExpand(parentId);
386         }
387 
388         int insertionIndex = visibleIds.indexOf(itemId) + 1;
389         List<String> inserts = new ArrayList<>();
390 
391         Resource parentResource = origin.getByPath(pathToExpand);
392         inserts.addAll(Lists.transform(parentResource.listChildren(), resourceToPath()));
393 
394         int i = 0;
395         while (i < inserts.size()) {
396             String child = inserts.get(i);
397             i++;
398             if (expandedIds.contains(child)) {
399                 inserts.addAll(i, Lists.transform(origin.getByPath(child).listChildren(), resourceToPath()));
400             }
401         }
402         visibleIds.addAll(insertionIndex, inserts);
403         expandedIds.add(pathToExpand);
404     }
405 
406     protected void doCollapse(Object itemId) {
407         int fromIndex = visibleIds.indexOf(itemId) + 1;
408         // Item is the last in container, nothing to collapse
409         if (fromIndex == visibleIds.size()) {
410             return;
411         }
412 
413         int toIndex = fromIndex;
414         // Find first non-descendant of itemId
415         Iterator<String> it = visibleIds.subList(fromIndex, visibleIds.size()).iterator();
416         List<String> ancestors = new ArrayList<>(Arrays.asList((String) itemId));
417         while (it.hasNext()) {
418             String nextPath = it.next();
419 
420             Resource resource = origin.getByPath(nextPath);
421 
422             if (ancestors.contains(resource.getParent().getPath())) {
423                 toIndex++;
424                 ancestors.add(nextPath);
425             } else {
426                 break;
427             }
428         }
429         visibleIds.subList(fromIndex, toIndex).clear();
430         expandedIds.remove(itemId);
431     }
432 
433     @Override
434     public boolean isCollapsed(Object itemId) {
435         return !expandedIds.contains(itemId);
436     }
437 
438     protected void showRootsOnly() {
439         this.visibleIds.clear();
440         this.visibleIds.addAll(rootItemIds());
441         this.expandedIds.clear();
442 
443         fireItemSetChange();
444     }
445 
446     // REFRESHABLE
447     @Override
448     public void refresh() {
449         final Predicate<String> originHasPath = new Predicate<String>() {
450             @Override
451             public boolean apply(@Nullable String input) {
452                 return origin.hasPath(input);
453             }
454         };
455 
456         this.visibleIds.clear();
457         this.cache.invalidateAll();
458         this.visibleIds.addAll(rootItemIds());
459 
460         final HashSet<String> expandedIdsCopy = Sets.newHashSet(Iterables.filter(expandedIds, originHasPath));
461         expandedIds.clear();
462 
463         for (final String expandedId : expandedIdsCopy) {
464             doExpand(expandedId);
465         }
466 
467         fireItemSetChange();
468     }
469 
470     // WRITE OPERATIONS
471 
472     @Override
473     public Item addItem(Object itemId) throws UnsupportedOperationException {
474         throw new UnsupportedOperationException(UNSUPPORTED_WRITE_OPERATION);
475     }
476 
477     @Override
478     public Object addItem() throws UnsupportedOperationException {
479         throw new UnsupportedOperationException(UNSUPPORTED_WRITE_OPERATION);
480     }
481 
482     @Override
483     public boolean removeItem(Object itemId) throws UnsupportedOperationException {
484         throw new UnsupportedOperationException(UNSUPPORTED_WRITE_OPERATION);
485     }
486 
487     @Override
488     public boolean removeAllItems() throws UnsupportedOperationException {
489         throw new UnsupportedOperationException(UNSUPPORTED_WRITE_OPERATION);
490     }
491 
492     @Override
493     public Object addItemAfter(Object previousItemId) throws UnsupportedOperationException {
494         throw new UnsupportedOperationException(UNSUPPORTED_WRITE_OPERATION);
495     }
496 
497     @Override
498     public Item addItemAfter(Object previousItemId, Object newItemId) throws UnsupportedOperationException {
499         throw new UnsupportedOperationException(UNSUPPORTED_WRITE_OPERATION);
500     }
501 
502     @Override
503     public Object addItemAt(int index) throws UnsupportedOperationException {
504         throw new UnsupportedOperationException(UNSUPPORTED_WRITE_OPERATION);
505     }
506 
507     @Override
508     public Item addItemAt(int index, Object newItemId) throws UnsupportedOperationException {
509         throw new UnsupportedOperationException(UNSUPPORTED_WRITE_OPERATION);
510     }
511 
512     @Override
513     public boolean setParent(Object itemId, Object newParentId) throws UnsupportedOperationException {
514         throw new UnsupportedOperationException(UNSUPPORTED_WRITE_OPERATION);
515     }
516 
517     @Override
518     public boolean setChildrenAllowed(Object itemId, boolean areChildrenAllowed) throws UnsupportedOperationException {
519         throw new UnsupportedOperationException(UNSUPPORTED_WRITE_OPERATION);
520     }
521 
522     // OVERRIDES to honor Container.ItemSetChangeNotifier's public interface (AbstractContainer defines those methods as protected)
523 
524     @Override
525     public void addItemSetChangeListener(ItemSetChangeListener listener) {
526         super.addItemSetChangeListener(listener);
527     }
528 
529     @Override
530     public void addListener(ItemSetChangeListener listener) {
531         super.addListener(listener);
532     }
533 
534     @Override
535     public void removeItemSetChangeListener(ItemSetChangeListener listener) {
536         super.removeItemSetChangeListener(listener);
537     }
538 
539     @Override
540     public void removeListener(ItemSetChangeListener listener) {
541         super.removeListener(listener);
542     }
543 
544     private Function<Resource, String> resourceToPath() {
545         return new Function<Resource, String>() {
546             @Nullable
547             @Override
548             public String apply(Resource resource) {
549                 return resource.getPath();
550             }
551         };
552     }
553 
554     private LoadingCache<String, Item> initializeCache() {
555         return CacheBuilder
556                 .newBuilder()
557                 .maximumSize(DEFAULT_ITEM_CACHE_SIZE)
558                 .build(new CacheLoader<String, Item>() {
559                     @Override
560                     public Item load(String itemId) throws Exception {
561                         return createItem(itemId);
562                     }
563                 });
564     }
565 }