View Javadoc

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