View Javadoc
1   /**
2    * This file Copyright (c) 2012-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.ui.contentapp.browser;
35  
36  import info.magnolia.event.EventBus;
37  import info.magnolia.ui.actionbar.ActionbarPresenter;
38  import info.magnolia.ui.actionbar.definition.ActionbarDefinition;
39  import info.magnolia.ui.actionbar.definition.ActionbarGroupDefinition;
40  import info.magnolia.ui.actionbar.definition.ActionbarItemDefinition;
41  import info.magnolia.ui.actionbar.definition.ActionbarSectionDefinition;
42  import info.magnolia.ui.api.action.ActionDefinition;
43  import info.magnolia.ui.api.action.ActionExecutor;
44  import info.magnolia.ui.api.app.SubAppContext;
45  import info.magnolia.ui.api.app.SubAppEventBus;
46  import info.magnolia.ui.api.availability.AvailabilityChecker;
47  import info.magnolia.ui.api.availability.AvailabilityDefinition;
48  import info.magnolia.ui.api.event.AdmincentralEventBus;
49  import info.magnolia.ui.api.location.Location;
50  import info.magnolia.ui.api.location.LocationChangedEvent;
51  import info.magnolia.ui.contentapp.ContentSubAppView;
52  import info.magnolia.ui.framework.app.BaseSubApp;
53  import info.magnolia.ui.vaadin.actionbar.ActionPopup;
54  import info.magnolia.ui.vaadin.integration.contentconnector.ContentConnector;
55  import info.magnolia.ui.workbench.event.ItemRightClickedEvent;
56  import info.magnolia.ui.workbench.event.SearchEvent;
57  import info.magnolia.ui.workbench.event.SelectionChangedEvent;
58  import info.magnolia.ui.workbench.event.ViewTypeChangedEvent;
59  import info.magnolia.ui.workbench.search.SearchPresenterDefinition;
60  
61  import java.util.Arrays;
62  import java.util.Iterator;
63  import java.util.List;
64  import java.util.Set;
65  
66  import javax.inject.Inject;
67  import javax.inject.Named;
68  
69  import org.apache.commons.lang3.StringUtils;
70  import org.slf4j.Logger;
71  import org.slf4j.LoggerFactory;
72  
73  import com.vaadin.ui.MenuBar.MenuItem;
74  import com.vaadin.server.ExternalResource;
75  
76  /**
77   * 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}.
78   *
79   * <pre>
80   *  <p>
81   *      This class Provides sensible implementation for services shared by all content subApps.
82   *      Out-of-the-box it will handle the following:
83   *  </p>
84   *
85   *  <ul>
86   *      <li>location updates when switching views, selecting items or performing searches: see {@link #locationChanged(Location)}
87   *      <li>restoring the browser app status when i.e. coming from a bookmark: see {@link #start(Location)}
88   *  </ul>
89   * In order to perform those tasks this class registers non-overridable handlers for the following events:
90   *  <ul>
91   *      <li> {@link SelectionChangedEvent}
92   *      <li> {@link ViewTypeChangedEvent}
93   *      <li> {@link SearchEvent}
94   *  </ul>
95   * Subclasses can augment the default behavior and perform additional tasks by overriding the following methods:
96   *  <ul>
97   *      <li>{@link #onSubAppStart()}
98   *      <li>{@link #locationChanged(Location)}
99   *      <li>{@link #updateActionbar(ActionbarPresenter)}
100  *  </ul>
101  * </pre>
102  *
103  *
104  * @deprecated since 6.2 - use {@link info.magnolia.ui.contentapp.ContentBrowserSubApp} instead.
105  *
106  * @see <a href="https://documentation.magnolia-cms.com/display/DOCS62/Upgrading+to+Magnolia+6.2.x">Upgrading to Magnolia 6.2.x</a>
107  *
108  * @see BrowserPresenter
109  * @see info.magnolia.ui.contentapp.ContentSubAppView
110  * @see info.magnolia.ui.contentapp.ContentApp
111  * @see BrowserLocation
112  */
113 @Deprecated
114 public class BrowserSubApp extends BaseSubApp<ContentSubAppView> {
115 
116     private static final Logger log = LoggerFactory.getLogger(BrowserSubApp.class);
117 
118     private final BrowserPresenter browser;
119     private final EventBus subAppEventBus;
120     private ActionExecutor actionExecutor;
121     protected ContentConnector contentConnector;
122     private AvailabilityChecker checker;
123 
124     @Inject
125     public BrowserSubApp(ActionExecutor actionExecutor, final SubAppContext subAppContext, final ContentSubAppView view, final BrowserPresenter browser, final @Named(SubAppEventBus.NAME) EventBus subAppEventBus, @Named(AdmincentralEventBus.NAME) EventBus adminCentralEventBus, ContentConnector contentConnector, AvailabilityChecker checker) {
126         super(subAppContext, view);
127         this.checker = checker;
128         if (subAppContext == null || view == null || browser == null || subAppEventBus == null) {
129             throw new IllegalArgumentException("Constructor does not allow for null args. Found SubAppContext = " + subAppContext + ", ContentSubAppView = " + view + ", BrowserPresenter = " + browser + ", EventBus = " + subAppEventBus);
130         }
131         this.browser = browser;
132         this.subAppEventBus = subAppEventBus;
133         this.actionExecutor = actionExecutor;
134         this.contentConnector = contentConnector;
135 
136 
137         /**
138          * Would be clearer if we'd track {@link info.magnolia.ui.api.app.AppLifecycleEventType#FOCUSED},
139          * but it is not reliable enough (not fired in some essential cases, see issue MGNLUI-2988 for more details).
140          * @see <a href="http://jira.magnolia-cms.com/browse/MGNLUI-2988"/>
141          */
142         adminCentralEventBus.addHandler(LocationChangedEvent.class, new LocationChangedEvent.Handler() {
143             @Override
144             public void onLocationChanged(LocationChangedEvent event) {
145                 /**
146                  * If the new location actually points to this sub-app then we refresh it in order to avoid
147                  * synchronisation issues caused by the changes in the data-source that were not propagated to
148                  * underlying Vaadin containers of the browser.
149                  */
150                 if (event.getNewLocation().equals(getCurrentLocation())) {
151                         restoreBrowser(getCurrentLocation());
152                 }
153             }
154         });
155     }
156 
157     /**
158      * Performs some routine tasks needed by all content subapps before the view is displayed.
159      * The tasks are:
160      * <ul>
161      * <li>setting the current location
162      * <li>setting the browser view
163      * <li>restoring the browser status: see {@link #restoreBrowser(BrowserLocation)}
164      * <li>calling {@link #onSubAppStart()} a hook-up method subclasses can override to perform additional work.
165      * </ul>
166      */
167     @Override
168     public ContentSubAppView start(final Location location) {
169         BrowserLocation l = BrowserLocation.wrap(location);
170         super.start(l);
171         getView().setContentView(browser.start());
172         restoreBrowser(l);
173         registerSubAppEventsHandlers(subAppEventBus);
174 
175         return getView();
176     }
177 
178     /**
179      * 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
180      * bookmark. I.e. given a bookmark containing the following URI fragment
181      * <p>
182      * {@code
183      * #app:myapp:browser;/foo/bar:list
184      * }
185      * <p>
186      * 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.
187      * <p>
188      * In case of a search view the URI fragment will look similar to the following one {@code
189      * #app:myapp:browser;/:search:qux
190      * }
191      * <p>
192      * 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.
193      *
194      * @see BrowserSubApp#updateActionbar(info.magnolia.ui.actionbar.ActionbarPresenter)
195      * @see BrowserSubApp#start(Location)
196      * @see Location
197      */
198     protected void restoreBrowser(final BrowserLocation location) {
199         String path = location.getNodePath();
200         String viewType = location.getViewType();
201 
202         if (!getBrowser().hasViewType(viewType)) {
203             if (!StringUtils.isBlank(viewType)) {
204                 log.warn("Unknown view type [{}], returning to default view type.", viewType);
205             }
206             viewType = getBrowser().getDefaultViewType();
207             location.updateViewType(viewType);
208             getAppContext().updateSubAppLocation(getSubAppContext(), location);
209         }
210         String query = location.getQuery();
211 
212         Object itemId = contentConnector.getItemIdByUrlFragment(path);
213 
214         // MGNLUI-1475: item might have not been found if path doesn't exist
215         if (!contentConnector.canHandleItem(itemId)) {
216             itemId = contentConnector.getDefaultItemId();
217             BrowserLocation newLocation = getCurrentLocation();
218             newLocation.updateNodePath("/");
219             getAppContext().updateSubAppLocation(getSubAppContext(), newLocation);
220         }
221 
222         getBrowser().resync(Arrays.asList(itemId), viewType, query);
223         updateActionbar(getBrowser().getActionbarPresenter());
224     }
225 
226 
227     /**
228      * Show the actionPopup for the specified item at the specified coordinates.
229      */
230     public void showActionPopup(Object itemId, int x, int y) {
231 
232         // If there's no actionbar configured we don't want to show an empty action popup
233         BrowserSubAppDescriptora/ui/contentapp/browser/BrowserSubAppDescriptor.html#BrowserSubAppDescriptor">BrowserSubAppDescriptor subAppDescriptor = (BrowserSubAppDescriptor) getSubAppContext().getSubAppDescriptor();
234         ActionbarDefinition actionbarDefinition = subAppDescriptor.getActionbar();
235         if (actionbarDefinition == null) {
236             return;
237         }
238 
239         ActionPopup actionPopup = browser.getView().getActionPopup();
240 
241         updateActionPopup(actionPopup);
242         actionPopup.open(x, y);
243     }
244 
245     /**
246      * Update the items in the actionPopup based on the selected item and the ActionPopup availability configuration.
247      * This method can be overriden to implement custom conditions diverging from {@link #updateActionbar(info.magnolia.ui.actionbar.ActionbarPresenter)}.
248      */
249     private void updateActionPopup(ActionPopup actionPopup) {
250 
251         actionPopup.removeItems();
252 
253         BrowserSubAppDescriptora/ui/contentapp/browser/BrowserSubAppDescriptor.html#BrowserSubAppDescriptor">BrowserSubAppDescriptor subAppDescriptor = (BrowserSubAppDescriptor) getSubAppContext().getSubAppDescriptor();
254         ActionbarDefinition actionbarDefinition = subAppDescriptor.getActionbar();
255         if (actionbarDefinition == null) {
256             return;
257         }
258         List<ActionbarSectionDefinition> sections = actionbarDefinition.getSections();
259 
260         // Figure out which section to show, only one
261         ActionbarSectionDefinition sectionDefinition = getVisibleSection(sections);
262 
263         // If there no section matched the selection we just hide everything
264         if (sectionDefinition == null) {
265             return;
266         }
267 
268         // Evaluate availability of each action within the section
269         for (final Iterator<ActionbarGroupDefinition> groupDefinitionIterator = sectionDefinition.getGroups().iterator(); groupDefinitionIterator.hasNext();) {
270             final ActionbarGroupDefinition groupDefinition = groupDefinitionIterator.next();
271             for (ActionbarItemDefinition itemDefinition : groupDefinition.getItems()) {
272 
273                 final String actionName = itemDefinition.getName();
274                 final MenuItem menuItem = addActionPopupItem(subAppDescriptor, itemDefinition);
275                 ActionDefinition actionDefinition = actionExecutor.getActionDefinition(actionName);
276                 if (actionDefinition != null) {
277                     AvailabilityDefinition availability = actionDefinition.getAvailability();
278                     menuItem.setEnabled(checker.isAvailable(availability, browser.getSelectedItemIds()));
279                 }
280             }
281 
282             // Add group separator.
283             if (groupDefinitionIterator.hasNext()) {
284                 getBrowser().getView().getActionPopup().addSeparator();
285             }
286         }
287     }
288 
289     /**
290      * Add an additional menu item on the actionPopup.
291      */
292     private MenuItem addActionPopupItem(BrowserSubAppDescriptor subAppDescriptor, ActionbarItemDefinition itemDefinition) {
293         String actionName = itemDefinition.getName();
294 
295         ActionDefinition action = subAppDescriptor.getActions().get(actionName);
296         String label = action.getLabel();
297         String iconFontCode = ActionPopup.ICON_FONT_CODE + action.getIcon();
298         ExternalResource iconFontResource = new ExternalResource(iconFontCode);
299 
300         return getBrowser().getView().getActionPopup().addItem(label, iconFontResource, item -> getBrowser().onActionbarItemClicked(actionName));
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 info.magnolia.ui.actionbar.ActionbarPresenter
310      */
311     public void updateActionbar(ActionbarPresenter actionbar) {
312 
313         BrowserSubAppDescriptora/ui/contentapp/browser/BrowserSubAppDescriptor.html#BrowserSubAppDescriptor">BrowserSubAppDescriptor subAppDescriptor = (BrowserSubAppDescriptor) getSubAppContext().getSubAppDescriptor();
314         ActionbarDefinition actionbarDefinition = subAppDescriptor.getActionbar();
315         if (actionbarDefinition == null) {
316             return;
317         }
318         List<ActionbarSectionDefinition> sections = actionbarDefinition.getSections();
319         // Figure out which section to show, only one
320         ActionbarSectionDefinition sectionDefinition = getVisibleSection(sections);
321 
322         // Hide all other sections
323         for (ActionbarSectionDefinition section : sections) {
324             actionbar.hideSection(section.getName());
325         }
326 
327         if (sectionDefinition != null) {
328             // Show our section
329             actionbar.showSection(sectionDefinition.getName());
330 
331             // Evaluate availability of each action within the section
332             for (ActionbarGroupDefinition groupDefinition : sectionDefinition.getGroups()) {
333                 for (ActionbarItemDefinition itemDefinition : groupDefinition.getItems()) {
334 
335                     String actionName = itemDefinition.getName();
336                     ActionDefinition actionDefinition = actionExecutor.getActionDefinition(actionName);
337                     if (actionDefinition != null) {
338                         AvailabilityDefinition availability = actionDefinition.getAvailability();
339                         if (checker.isAvailable(availability, browser.getSelectedItemIds())) {
340                             actionbar.enable(actionName);
341                         } else {
342                             actionbar.disable(actionName);
343                         }
344                     } else {
345                         log.warn("Action bar expected an action named {}, but no such action is currently configured.", actionName);
346                     }
347                 }
348             }
349         }
350     }
351 
352     private ActionbarSectionDefinition getVisibleSection(List<ActionbarSectionDefinition> sections) {
353         for (ActionbarSectionDefinition section : sections) {
354             if (isSectionVisible(section))
355                 return section;
356         }
357         return null;
358     }
359 
360     private boolean isSectionVisible(ActionbarSectionDefinition section) {
361         return checker.isAvailable(section.getAvailability(), browser.getSelectedItemIds());
362     }
363 
364     protected final BrowserPresenter getBrowser() {
365         return browser;
366     }
367 
368     /**
369      * The default implementation selects the path in the current workspace and updates the available actions in the actionbar.
370      */
371     @Override
372     public void locationChanged(final Location location) {
373         super.locationChanged(location);
374         restoreBrowser(BrowserLocation.wrap(location));
375     }
376 
377     /**
378      * Wraps the current DefaultLocation in a {@link BrowserLocation}. Providing getter and setters for used parameters.
379      */
380     @Override
381     public BrowserLocation getCurrentLocation() {
382         return BrowserLocation.wrap(super.getCurrentLocation());
383     }
384 
385     /*
386      * Registers general purpose handlers for the following events:
387      * <ul>
388      * <li> {@link ItemSelectedEvent}
389      * <li> {@link ViewTypeChangedEvent}
390      * <li> {@link SearchEvent}
391      * </ul>
392      */
393     private void registerSubAppEventsHandlers(final EventBus subAppEventBus) {
394         final ActionbarPresenter actionbar = getBrowser().getActionbarPresenter();
395         subAppEventBus.addHandler(SelectionChangedEvent.class, new SelectionChangedEvent.Handler() {
396 
397             @Override
398             public void onSelectionChanged(SelectionChangedEvent event) {
399                 handleSelectionChange(event.getItemIds(), actionbar);
400             }
401         });
402 
403         subAppEventBus.addHandler(ItemRightClickedEvent.class, new ItemRightClickedEvent.Handler() {
404 
405             @Override
406             public void onItemRightClicked(ItemRightClickedEvent event) {
407                 showActionPopup(event.getItemId(), event.getClickX(), event.getClickY());
408             }
409         });
410 
411         subAppEventBus.addHandler(ViewTypeChangedEvent.class, new ViewTypeChangedEvent.Handler() {
412 
413             @Override
414             public void onViewChanged(ViewTypeChangedEvent event) {
415                 BrowserLocation location = getCurrentLocation();
416                 // remove search term from fragment when switching back
417                 if (location.getViewType().equals(SearchPresenterDefinition.VIEW_TYPE)
418                         && !event.getViewType().equals(SearchPresenterDefinition.VIEW_TYPE)) {
419                     location.updateQuery("");
420                 }
421                 location.updateViewType(event.getViewType());
422                 getAppContext().updateSubAppLocation(getSubAppContext(), location);
423                 updateActionbar(actionbar);
424             }
425         });
426 
427         subAppEventBus.addHandler(SearchEvent.class, new SearchEvent.Handler() {
428 
429             @Override
430             public void onSearch(SearchEvent event) {
431                 BrowserLocation location = getCurrentLocation();
432                 if (StringUtils.isNotBlank(event.getSearchExpression())) {
433                     location.updateViewType(SearchPresenterDefinition.VIEW_TYPE);
434                 }
435                 location.updateQuery(event.getSearchExpression());
436                 getAppContext().updateSubAppLocation(getSubAppContext(), location);
437                 updateActionbar(actionbar);
438             }
439         });
440     }
441 
442     private void handleSelectionChange(Set<Object> selectionIds, ActionbarPresenter actionbar) {
443         BrowserLocation location = getCurrentLocation();
444         applySelectionToLocation(location, selectionIds.isEmpty() ? contentConnector.getDefaultItemId() : selectionIds.iterator().next());
445         getAppContext().updateSubAppLocation(getSubAppContext(), location);
446         updateActionbar(actionbar);
447 
448     }
449 
450     protected void applySelectionToLocation(BrowserLocation location, Object selectedId) {
451         location.updateNodePath("");
452         if (!contentConnector.canHandleItem(selectedId)) {
453             // nothing is selected at the moment
454         } else {
455             location.updateNodePath(contentConnector.getItemUrlFragment(selectedId));
456         }
457     }
458 }