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