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