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