View Javadoc
1   /**
2    * This file Copyright (c) 2012-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.ui.contentapp.browser;
35  
36  import info.magnolia.cms.security.Permission;
37  import info.magnolia.cms.security.PermissionUtil;
38  import info.magnolia.cms.security.operations.AccessDefinition;
39  import info.magnolia.context.MgnlContext;
40  import info.magnolia.event.EventBus;
41  import info.magnolia.jcr.util.NodeUtil;
42  import info.magnolia.jcr.util.SessionUtil;
43  import info.magnolia.objectfactory.ComponentProvider;
44  import info.magnolia.ui.actionbar.ActionbarPresenter;
45  import info.magnolia.ui.actionbar.definition.ActionbarDefinition;
46  import info.magnolia.ui.actionbar.definition.ActionbarGroupDefinition;
47  import info.magnolia.ui.actionbar.definition.ActionbarItemDefinition;
48  import info.magnolia.ui.actionbar.definition.ActionbarSectionDefinition;
49  import info.magnolia.ui.api.action.ActionDefinition;
50  import info.magnolia.ui.api.action.ActionExecutor;
51  import info.magnolia.ui.api.app.SubAppContext;
52  import info.magnolia.ui.api.app.SubAppEventBus;
53  import info.magnolia.ui.api.availability.AvailabilityDefinition;
54  import info.magnolia.ui.api.availability.AvailabilityRule;
55  import info.magnolia.ui.api.location.Location;
56  import info.magnolia.ui.contentapp.ContentSubAppView;
57  import info.magnolia.ui.framework.app.BaseSubApp;
58  import info.magnolia.ui.vaadin.actionbar.ActionPopup;
59  import info.magnolia.ui.vaadin.integration.jcr.JcrItemUtil;
60  import info.magnolia.ui.workbench.definition.WorkbenchDefinition;
61  import info.magnolia.ui.workbench.event.ItemRightClickedEvent;
62  import info.magnolia.ui.workbench.event.SearchEvent;
63  import info.magnolia.ui.workbench.event.SelectionChangedEvent;
64  import info.magnolia.ui.workbench.event.ViewTypeChangedEvent;
65  import info.magnolia.ui.workbench.search.SearchPresenterDefinition;
66  
67  import java.util.ArrayList;
68  import java.util.List;
69  
70  import javax.inject.Inject;
71  import javax.inject.Named;
72  import javax.jcr.Item;
73  import javax.jcr.Node;
74  import javax.jcr.Property;
75  import javax.jcr.RepositoryException;
76  
77  import org.apache.commons.lang.StringUtils;
78  import org.slf4j.Logger;
79  import org.slf4j.LoggerFactory;
80  import org.vaadin.peter.contextmenu.ContextMenu;
81  
82  import com.vaadin.server.ExternalResource;
83  
84  /**
85   * Base implementation of a content subApp. A content subApp displays a collection of data represented inside a {@link info.magnolia.ui.workbench.ContentView} created by the {@link info.magnolia.ui.workbench.WorkbenchPresenter}.
86   * 
87   * <pre>
88   *  <p>
89   *      This class Provides sensible implementation for services shared by all content subApps.
90   *      Out-of-the-box it will handle the following:
91   *  </p>
92   * 
93   *  <ul>
94   *      <li>location updates when switching views, selecting items or performing searches: see {@link #locationChanged(Location)}
95   *      <li>restoring the browser app status when i.e. coming from a bookmark: see {@link #start(Location)}
96   *  </ul>
97   * In order to perform those tasks this class registers non-overridable handlers for the following events:
98   *  <ul>
99   *      <li> {@link SelectionChangedEvent}
100  *      <li> {@link ViewTypeChangedEvent}
101  *      <li> {@link SearchEvent}
102  *  </ul>
103  * Subclasses can augment the default behavior and perform additional tasks by overriding the following methods:
104  *  <ul>
105  *      <li>{@link #onSubAppStart()}
106  *      <li>{@link #locationChanged(Location)}
107  *      <li>{@link #updateActionbar(ActionbarPresenter)}
108  *  </ul>
109  * </pre>
110  * 
111  * @see BrowserPresenter
112  * @see info.magnolia.ui.contentapp.ContentSubAppView
113  * @see info.magnolia.ui.contentapp.ContentApp
114  * @see BrowserLocation
115  */
116 public class BrowserSubApp extends BaseSubApp<ContentSubAppView> {
117 
118     private static final Logger log = LoggerFactory.getLogger(BrowserSubApp.class);
119 
120     private final BrowserPresenter browser;
121     private final EventBus subAppEventBus;
122     private ActionExecutor actionExecutor;
123     private ComponentProvider componentProvider;
124     private String workbenchRoot;
125 
126     @Inject
127     public BrowserSubApp(ActionExecutor actionExecutor, final SubAppContext subAppContext, final ContentSubAppView view, final BrowserPresenter browser, final @Named(SubAppEventBus.NAME) EventBus subAppEventBus, final ComponentProvider componentProvider) {
128         super(subAppContext, view);
129         if (subAppContext == null || view == null || browser == null || subAppEventBus == null) {
130             throw new IllegalArgumentException("Constructor does not allow for null args. Found SubAppContext = " + subAppContext + ", ContentSubAppView = " + view + ", BrowserPresenter = " + browser + ", EventBus = " + subAppEventBus);
131         }
132         this.browser = browser;
133         this.subAppEventBus = subAppEventBus;
134         this.actionExecutor = actionExecutor;
135         this.componentProvider = componentProvider;
136         this.workbenchRoot = ((BrowserSubAppDescriptor) subAppContext.getSubAppDescriptor()).getWorkbench().getPath();
137     }
138 
139     /**
140      * Performs some routine tasks needed by all content subapps before the view is displayed.
141      * The tasks are:
142      * <ul>
143      * <li>setting the current location
144      * <li>setting the browser view
145      * <li>restoring the browser status: see {@link #restoreBrowser(BrowserLocation)}
146      * <li>calling {@link #onSubAppStart()} a hook-up method subclasses can override to perform additional work.
147      * </ul>
148      */
149     @Override
150     public ContentSubAppView start(final Location location) {
151         BrowserLocation l = BrowserLocation.wrap(location);
152         super.start(l);
153         getView().setContentView(browser.start());
154         restoreBrowser(l);
155         registerSubAppEventsHandlers(subAppEventBus, this);
156 
157         return getView();
158     }
159 
160     /**
161      * Restores the browser status based on the information available in the location object. This is used e.g. when starting a subapp based on a
162      * bookmark. I.e. given a bookmark containing the following URI fragment
163      * <p>
164      * {@code
165      * #app:myapp:browser;/foo/bar:list
166      * }
167      * <p>
168      * this method will select the path <code>/foo/bar</code> in the workspace used by the app, set the view type as <code>list</code> and finally update the available actions.
169      * <p>
170      * In case of a search view the URI fragment will look similar to the following one {@code
171      * #app:myapp:browser;/:search:qux
172      * }
173      * <p>
174      * then this method will select the root path, set the view type as <code>search</code>, perform a search for "qux" in the workspace used by the app and finally update the available actions.
175      * 
176      * @see BrowserSubApp#updateActionbar(ActionbarPresenter)
177      * @see BrowserSubApp#start(Location)
178      * @see Location
179      */
180     protected void restoreBrowser(final BrowserLocation location) {
181         String path = ("/".equals(workbenchRoot) ? "" : workbenchRoot) + location.getNodePath();
182         String viewType = location.getViewType();
183 
184         if (!getBrowser().hasViewType(viewType)) {
185             if (!StringUtils.isBlank(viewType)) {
186                 log.warn("Unknown view type [{}], returning to default view type.", viewType);
187             }
188             viewType = getBrowser().getDefaultViewType();
189             location.updateViewType(viewType);
190             getAppContext().updateSubAppLocation(getSubAppContext(), location);
191         }
192         String query = location.getQuery();
193 
194         BrowserSubAppDescriptor subAppDescriptor = (BrowserSubAppDescriptor) getSubAppContext().getSubAppDescriptor();
195         final String workspaceName = subAppDescriptor.getWorkbench().getWorkspace();
196 
197         String itemId = null;
198         try {
199             itemId = JcrItemUtil.getItemId(SessionUtil.getNode(workspaceName, path));
200 
201             // MGNLUI-1475: item might have not been found if path doesn't exist
202             if (itemId == null) {
203                 itemId = JcrItemUtil.getItemId(SessionUtil.getNode(workspaceName, workbenchRoot));
204                 BrowserLocation newLocation = getCurrentLocation();
205                 newLocation.updateNodePath("/");
206 
207                 getAppContext().updateSubAppLocation(getSubAppContext(), newLocation);
208             }
209         } catch (RepositoryException e) {
210             log.warn("Could not retrieve item at path {} in workspace {}", path, workspaceName);
211         }
212         List<String> ids = new ArrayList<String>();
213         if (itemId != null) {
214             ids.add(itemId);
215         }
216         getBrowser().resync(ids, viewType, query);
217         updateActionbar(getBrowser().getActionbarPresenter());
218     }
219 
220     /**
221      * Show the actionPopup for the specified item at the specified coordinates.
222      */
223     public void showActionPopup(String absItemPath, int x, int y) {
224 
225         // If there's no actionbar configured we don't want to show an empty action popup
226         BrowserSubAppDescriptor subAppDescriptor = (BrowserSubAppDescriptor) getSubAppContext().getSubAppDescriptor();
227         ActionbarDefinition actionbarDefinition = subAppDescriptor.getActionbar();
228         if (actionbarDefinition == null) {
229             return;
230         }
231 
232         ActionPopup actionPopup = browser.getView().getActionPopup();
233 
234         updateActionPopup(actionPopup);
235         actionPopup.open(x, y);
236     }
237 
238     /**
239      * Update the items in the actionPopup based on the selected item and the ActionPopup availability configuration.
240      * This method can be overriden to implement custom conditions diverging from {@link #updateActionbar(info.magnolia.ui.actionbar.ActionbarPresenter)}.
241      */
242     private void updateActionPopup(ActionPopup actionPopup) {
243 
244         actionPopup.removeAllItems();
245 
246         BrowserSubAppDescriptor subAppDescriptor = (BrowserSubAppDescriptor) getSubAppContext().getSubAppDescriptor();
247         WorkbenchDefinition workbench = subAppDescriptor.getWorkbench();
248         ActionbarDefinition actionbarDefinition = subAppDescriptor.getActionbar();
249         if (actionbarDefinition == null) {
250             return;
251         }
252         List<ActionbarSectionDefinition> sections = actionbarDefinition.getSections();
253 
254         try {
255             String workbenchRootItemId = JcrItemUtil.getItemId(workbench.getWorkspace(), workbench.getPath());
256             List<String> selectedItemIds = getBrowser().getSelectedItemIds();
257             List<Item> items = getJcrItemsExceptOne(workbench.getWorkspace(), selectedItemIds, workbenchRootItemId);
258 
259             // Figure out which section to show, only one
260             ActionbarSectionDefinition sectionDefinition = getVisibleSection(sections, items);
261 
262             // If there no section matched the selection we just hide everything
263             if (sectionDefinition == null) {
264                 return;
265             }
266 
267             // Evaluate availability of each action within the section
268             ContextMenu.ContextMenuItem menuItem = null;
269             for (ActionbarGroupDefinition groupDefinition : sectionDefinition.getGroups()) {
270                 for (ActionbarItemDefinition itemDefinition : groupDefinition.getItems()) {
271 
272                     String actionName = itemDefinition.getName();
273                     menuItem = addActionPopupItem(subAppDescriptor, actionPopup, itemDefinition, items);
274                     menuItem.setEnabled(actionExecutor.isAvailable(actionName, items.toArray(new Item[items.size()])));
275                 }
276 
277                 // Add group separator.
278                 if (menuItem != null) {
279                     menuItem.setSeparatorVisible(true);
280                 }
281             }
282             if (menuItem != null) {
283                 menuItem.setSeparatorVisible(false);
284             }
285         } catch (RepositoryException e) {
286             log.error("Failed to updated actionbar", e);
287         }
288     }
289 
290     /**
291      * Add an additional menu item on the actionPopup.
292      */
293     private ContextMenu.ContextMenuItem addActionPopupItem(BrowserSubAppDescriptor subAppDescriptor, ActionPopup actionPopup, ActionbarItemDefinition itemDefinition, List<javax.jcr.Item> items) {
294         String actionName = itemDefinition.getName();
295 
296         ActionDefinition action = subAppDescriptor.getActions().get(actionName);
297         String label = action.getLabel();
298         String iconFontCode = ActionPopup.ICON_FONT_CODE + action.getIcon();
299         ExternalResource iconFontResource = new ExternalResource(iconFontCode);
300         ContextMenu.ContextMenuItem menuItem = actionPopup.addItem(label, iconFontResource);
301         // Set data variable so that the event handler can determine which action to launch.
302         menuItem.setData(actionName);
303 
304         return menuItem;
305     }
306 
307     /**
308      * Update the items in the actionbar based on the selected item and the action availability configuration.
309      * This method can be overriden to implement custom conditions diverging from {@link #updateActionPopup(info.magnolia.ui.vaadin.actionbar.ActionPopup)}.
310      * 
311      * @see #restoreBrowser(BrowserLocation)
312      * @see #locationChanged(Location)
313      * @see ActionbarPresenter
314      */
315     public void updateActionbar(ActionbarPresenter actionbar) {
316 
317         BrowserSubAppDescriptor subAppDescriptor = (BrowserSubAppDescriptor) getSubAppContext().getSubAppDescriptor();
318         WorkbenchDefinition workbench = subAppDescriptor.getWorkbench();
319         ActionbarDefinition actionbarDefinition = subAppDescriptor.getActionbar();
320         if (actionbarDefinition == null) {
321             return;
322         }
323         List<ActionbarSectionDefinition> sections = actionbarDefinition.getSections();
324 
325         try {
326             String workbenchRootItemId = JcrItemUtil.getItemId(workbench.getWorkspace(), workbench.getPath());
327             List<String> selectedItemIds = getBrowser().getSelectedItemIds();
328             List<Item> items = getJcrItemsExceptOne(workbench.getWorkspace(), selectedItemIds, workbenchRootItemId);
329 
330             // Figure out which section to show, only one
331             ActionbarSectionDefinition sectionDefinition = getVisibleSection(sections, items);
332 
333             // If there no section matched the selection we just hide everything
334             if (sectionDefinition == null) {
335                 for (ActionbarSectionDefinition section : sections) {
336                     actionbar.hideSection(section.getName());
337                 }
338                 return;
339             }
340 
341             // Hide all other sections
342             for (ActionbarSectionDefinition section : sections) {
343                 if (section != sectionDefinition) {
344                     actionbar.hideSection(section.getName());
345                 }
346             }
347 
348             // Show our section
349             actionbar.showSection(sectionDefinition.getName());
350 
351             // Evaluate availability of each action within the section
352             for (ActionbarGroupDefinition groupDefinition : sectionDefinition.getGroups()) {
353                 for (ActionbarItemDefinition itemDefinition : groupDefinition.getItems()) {
354 
355                     String actionName = itemDefinition.getName();
356                     if (actionExecutor.isAvailable(actionName, items.toArray(new Item[items.size()]))) {
357                         actionbar.enable(actionName);
358                     } else {
359                         actionbar.disable(actionName);
360                     }
361                 }
362             }
363         } catch (RepositoryException e) {
364             log.error("Failed to updated actionbar", e);
365             for (ActionbarSectionDefinition section : sections) {
366                 actionbar.hideSection(section.getName());
367             }
368         }
369     }
370 
371     private ActionbarSectionDefinition getVisibleSection(List<ActionbarSectionDefinition> sections, List<Item> items) throws RepositoryException {
372         for (ActionbarSectionDefinition section : sections) {
373             if (isSectionVisible(section, items))
374                 return section;
375         }
376         return null;
377     }
378 
379     private boolean isSectionVisible(ActionbarSectionDefinition section, List<Item> items) throws RepositoryException {
380         AvailabilityDefinition availability = section.getAvailability();
381 
382         // Validate that the user has all required roles - only once
383         if (!availability.getAccess().hasAccess(MgnlContext.getUser())) {
384             return false;
385         }
386 
387         if (items != null) {
388             // section must be visible for all items
389             for (Item item : items) {
390                 if (!isSectionVisible(section, item)) {
391                     return false;
392                 }
393             }
394         }
395         return true;
396     }
397 
398     private boolean isSectionVisible(ActionbarSectionDefinition section, Item item) throws RepositoryException {
399         AvailabilityDefinition availability = section.getAvailability();
400 
401         // if a rule class is set, verify it first
402         if ((availability.getRuleClass() != null)) {
403             // if the rule class cannot be instantiated, or the rule returns false
404             AvailabilityRule rule = componentProvider.newInstance(availability.getRuleClass());
405             if (rule == null || !rule.isAvailable(item)) {
406                 return false;
407             }
408         }
409 
410         // Evaluate write permission if specified
411         if (availability.isWritePermissionRequired() && !MgnlContext.getUser().hasRole(AccessDefinition.DEFAULT_SUPERUSER_ROLE)) {
412             try {
413                 Node node = item instanceof Property ? ((Property) item).getParent() : (Node) item;
414                 if (!PermissionUtil.isGranted(node, Permission.WRITE)) {
415                     return false;
416                 }
417             } catch (RepositoryException e) {
418                 log.warn("Could not evaluate write permission for {}.", item);
419             }
420         }
421 
422         // If this is the root item we display the section only if the root property is set
423         if (item == null) {
424             return availability.isRoot();
425         }
426 
427         // If its a property we display it only if the properties property is set
428         if (!item.isNode()) {
429             return availability.isProperties();
430         }
431 
432         // If node is selected and the section is available for nodes
433         if (availability.isNodes()) {
434             // if no node type defined, the for all node types
435             if (availability.getNodeTypes().isEmpty()) {
436                 return true;
437             }
438             // else the node must match at least one of the configured node types
439             for (String nodeType : availability.getNodeTypes()) {
440                 if (NodeUtil.isNodeType((Node) item, nodeType)) {
441                     return true;
442                 }
443             }
444 
445         }
446         return false;
447     }
448 
449     protected final BrowserPresenter getBrowser() {
450         return browser;
451     }
452 
453     /**
454      * The default implementation selects the path in the current workspace and updates the available actions in the actionbar.
455      */
456     @Override
457     public void locationChanged(final Location location) {
458         super.locationChanged(location);
459         restoreBrowser(BrowserLocation.wrap(location));
460     }
461 
462     /**
463      * Wraps the current DefaultLocation in a {@link BrowserLocation}. Providing getter and setters for used parameters.
464      */
465     @Override
466     public BrowserLocation getCurrentLocation() {
467         return BrowserLocation.wrap(super.getCurrentLocation());
468     }
469 
470     /*
471      * Registers general purpose handlers for the following events:
472      * <ul>
473      * <li> {@link ItemSelectedEvent}
474      * <li> {@link ViewTypeChangedEvent}
475      * <li> {@link SearchEvent}
476      * </ul>
477      */
478     private void registerSubAppEventsHandlers(final EventBus subAppEventBus, final BrowserSubApp subApp) {
479         final ActionbarPresenter actionbar = subApp.getBrowser().getActionbarPresenter();
480         subAppEventBus.addHandler(SelectionChangedEvent.class, new SelectionChangedEvent.Handler() {
481 
482             @Override
483             public void onSelectionChanged(SelectionChangedEvent event) {
484                 BrowserLocation location = getCurrentLocation();
485                 try {
486                     Item selected = JcrItemUtil.getJcrItem(event.getWorkspace(), JcrItemUtil.parseNodeIdentifier(event.getFirstItemId()));
487                     if (selected == null) {
488                         // nothing is selected at the moment
489                         location.updateNodePath("");
490                     } else {
491                         location.updateNodePath(StringUtils.removeStart(selected.getPath(), "/".equals(workbenchRoot) ? "" : workbenchRoot));
492                     }
493                 } catch (RepositoryException e) {
494                     log.warn("Could not get jcrItem with itemId " + event.getFirstItemId() + " from workspace " + event.getWorkspace(), e);
495                 }
496                 getAppContext().updateSubAppLocation(getSubAppContext(), location);
497                 updateActionbar(actionbar);
498             }
499         });
500 
501         subAppEventBus.addHandler(ItemRightClickedEvent.class, new ItemRightClickedEvent.Handler() {
502 
503             @Override
504             public void onItemRightClicked(ItemRightClickedEvent event) {
505                 String absItemPath;
506                 try {
507                     absItemPath = event.getItem().getJcrItem().getPath();
508 
509                     showActionPopup(absItemPath, event.getClickX(), event.getClickY());
510                 } catch (RepositoryException e) {
511                     log.warn("Could not get jcrItem with itemId " + event.getItemId() + " from workspace " + event.getWorkspace(), e);
512                 }
513 
514             }
515         });
516 
517         subAppEventBus.addHandler(ViewTypeChangedEvent.class, new ViewTypeChangedEvent.Handler() {
518 
519             @Override
520             public void onViewChanged(ViewTypeChangedEvent event) {
521                 BrowserLocation location = getCurrentLocation();
522                 // remove search term from fragment when switching back
523                 if (location.getViewType().equals(SearchPresenterDefinition.VIEW_TYPE)
524                         && !event.getViewType().equals(SearchPresenterDefinition.VIEW_TYPE)) {
525                     location.updateQuery("");
526                 }
527                 location.updateViewType(event.getViewType());
528                 getAppContext().updateSubAppLocation(getSubAppContext(), location);
529                 updateActionbar(actionbar);
530             }
531         });
532 
533         subAppEventBus.addHandler(SearchEvent.class, new SearchEvent.Handler() {
534 
535             @Override
536             public void onSearch(SearchEvent event) {
537                 BrowserLocation location = getCurrentLocation();
538                 if (StringUtils.isNotBlank(event.getSearchExpression())) {
539                     location.updateViewType(SearchPresenterDefinition.VIEW_TYPE);
540                 }
541                 location.updateQuery(event.getSearchExpression());
542                 getAppContext().updateSubAppLocation(getSubAppContext(), location);
543                 updateActionbar(actionbar);
544             }
545         });
546     }
547 
548     public static List<Item> getJcrItemsExceptOne(final String workspaceName, List<String> ids, String itemIdToExclude) {
549         List<Item> items = JcrItemUtil.getJcrItems(workspaceName, ids);
550         if (itemIdToExclude == null) {
551             return items;
552         }
553         for (int i = 0; i < items.size(); i++) {
554             try {
555                 if (itemIdToExclude.equals(JcrItemUtil.getItemId(items.get(i)))) {
556                     items.set(i, null);
557                 }
558             } catch (RepositoryException e) {
559                 log.debug("Cannot get item ID for item [{}].", items.get(i));
560             }
561         }
562         return items;
563     }
564 }