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