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.ActionbarView;
39  import info.magnolia.ui.actionbar.definition.ActionbarDefinition;
40  import info.magnolia.ui.api.action.ActionExecutionException;
41  import info.magnolia.ui.api.action.ActionExecutor;
42  import info.magnolia.ui.api.app.AppContext;
43  import info.magnolia.ui.api.app.SubAppContext;
44  import info.magnolia.ui.api.app.SubAppEventBus;
45  import info.magnolia.ui.api.availability.AvailabilityChecker;
46  import info.magnolia.ui.api.availability.AvailabilityDefinition;
47  import info.magnolia.ui.api.event.AdmincentralEventBus;
48  import info.magnolia.ui.api.event.ContentChangedEvent;
49  import info.magnolia.ui.api.message.Message;
50  import info.magnolia.ui.api.message.MessageType;
51  import info.magnolia.ui.imageprovider.ImageProvider;
52  import info.magnolia.ui.vaadin.integration.NullItem;
53  import info.magnolia.ui.vaadin.integration.contentconnector.ContentConnector;
54  import info.magnolia.ui.workbench.WorkbenchPresenter;
55  import info.magnolia.ui.workbench.WorkbenchView;
56  import info.magnolia.ui.workbench.event.ActionEvent;
57  import info.magnolia.ui.workbench.event.ItemDoubleClickedEvent;
58  import info.magnolia.ui.workbench.event.ItemShortcutKeyEvent;
59  import info.magnolia.ui.workbench.event.SearchEvent;
60  import info.magnolia.ui.workbench.event.SelectionChangedEvent;
61  
62  import java.util.ArrayList;
63  import java.util.Arrays;
64  import java.util.Collection;
65  import java.util.Iterator;
66  import java.util.List;
67  import java.util.Set;
68  
69  import javax.inject.Inject;
70  import javax.inject.Named;
71  
72  import org.apache.commons.collections4.ListUtils;
73  import org.apache.commons.collections4.Predicate;
74  import org.apache.commons.lang3.StringUtils;
75  import org.slf4j.Logger;
76  import org.slf4j.LoggerFactory;
77  
78  import com.vaadin.event.ShortcutAction;
79  import com.vaadin.server.Resource;
80  import com.vaadin.v7.data.Item;
81  
82  
83  /**
84   * The browser is a core component of AdminCentral. It represents the main hub through which users can interact with
85   * JCR data. It is compounded by three main sub-components:
86   * <ul>
87   * <li>a configurable data grid.
88   * <li>a configurable function toolbar on top of the data grid, providing operations such as switching from tree to list view or thumbnail view or performing searches on data.
89   * <li>a configurable action bar on the right hand side, showing the available operations for the given workspace and the selected item.
90   * </ul>
91   * <p>
92   * Its main configuration point is the {@link info.magnolia.ui.workbench.definition.WorkbenchDefinition} through which one defines the JCR workspace to connect to, the columns/properties to display, the available actions and so on.
93   */
94  public class BrowserPresenter implements ActionbarPresenter.Listener, BrowserView.Listener {
95  
96      private static final Logger log = LoggerFactory.getLogger(BrowserPresenter.class);
97  
98      private final Predicate<Object> itemExistsPredicate = this::verifyItemExists;
99  
100     private final WorkbenchPresenter workbenchPresenter;
101 
102     private final ActionExecutor actionExecutor;
103 
104     private final BrowserSubAppDescriptor subAppDescriptor;
105 
106     private final BrowserView view;
107 
108     private final EventBus admincentralEventBus;
109 
110     private final EventBus subAppEventBus;
111 
112     private final ActionbarPresenter actionbarPresenter;
113 
114     private final AvailabilityChecker availabilityChecker;
115 
116     private final AppContext appContext;
117 
118     private final ContentConnector contentConnector;
119 
120     private final ImageProvider imageProvider;
121 
122     @Inject
123     public BrowserPresenter(
124             BrowserView view,
125             SubAppContext subAppContext,
126             ActionExecutor actionExecutor,
127             @Named(AdmincentralEventBus.NAME) EventBus admincentralEventBus,
128             @Named(SubAppEventBus.NAME) EventBus subAppEventBus,
129             ContentConnector contentConnector,
130             ImageProvider imageProvider,
131             WorkbenchPresenter workbenchPresenter,
132             ActionbarPresenter actionbarPresenter,
133             AvailabilityChecker availabilityChecker) {
134         this.view = view;
135         this.appContext = subAppContext.getAppContext();
136         this.subAppDescriptor = (BrowserSubAppDescriptor) subAppContext.getSubAppDescriptor();
137         this.actionExecutor = actionExecutor;
138         this.admincentralEventBus = admincentralEventBus;
139         this.subAppEventBus = subAppEventBus;
140         this.contentConnector = contentConnector;
141         this.imageProvider = imageProvider;
142         this.workbenchPresenter = workbenchPresenter;
143         this.actionbarPresenter = actionbarPresenter;
144         this.availabilityChecker = availabilityChecker;
145     }
146 
147     public BrowserView start() {
148         actionbarPresenter.setListener(this);
149 
150         WorkbenchView workbenchView = workbenchPresenter.start(subAppDescriptor.getWorkbench(), subAppDescriptor.getImageProvider(), subAppEventBus);
151         ActionbarView actionbar = actionbarPresenter.start(subAppDescriptor.getActionbar(), subAppDescriptor.getActions());
152 
153         workbenchView.setTitle(subAppDescriptor.getLabel());
154 
155         view.setWorkbenchView(workbenchView);
156         view.setActionbarView(actionbar);
157         view.setListener(this);
158 
159         bindHandlers();
160         return view;
161     }
162 
163     protected void bindHandlers() {
164         admincentralEventBus.addHandler(ContentChangedEvent.class, event -> {
165             List<Object> itemIds = new ArrayList<>();
166             if (event.getItemId() instanceof Collection) {
167                 itemIds.addAll((Collection<?>) event.getItemId());
168             } else {
169                 itemIds.add(event.getItemId());
170             }
171             workbenchPresenter.refresh();
172             // filter out items that can't be handled or doesn't exist
173             // if an item passed in the event exists, mark it as selected (see MGNLUI-2919)
174             // otherwise preserve previous selection
175             List<Object> existingSelectedItemIds = ListUtils.select(itemIds, itemExistsPredicate);
176             if (existingSelectedItemIds.isEmpty()) {
177                 existingSelectedItemIds = ListUtils.select(getSelectedItemIds(), itemExistsPredicate);
178             }
179 
180             workbenchPresenter.select(existingSelectedItemIds);
181 
182             if (event.isItemContentChanged() && !existingSelectedItemIds.isEmpty()) {
183                 workbenchPresenter.expand(existingSelectedItemIds.get(0));
184             }
185 
186             // use just the first selected item to show the preview image
187             if (!existingSelectedItemIds.isEmpty()) {
188                 refreshActionbarPreviewImage(existingSelectedItemIds.get(0));
189             }
190         });
191 
192         subAppEventBus.addHandler(SelectionChangedEvent.class, event -> {
193             // if exactly one node is selected, use it for preview
194             refreshActionbarPreviewImage(event.getFirstItemId());
195         });
196 
197         subAppEventBus.addHandler(ItemDoubleClickedEvent.class, event -> executeDefaultAction());
198 
199         subAppEventBus.addHandler(SearchEvent.class, event -> workbenchPresenter.doSearch(event.getSearchExpression()));
200 
201         subAppEventBus.addHandler(ActionEvent.class, event -> executeAction(event.getActionName(), event.getItemIds(), event.getParameters()));
202 
203         subAppEventBus.addHandler(ItemShortcutKeyEvent.class, event -> {
204             int keyCode = event.getKeyCode();
205             switch (keyCode) {
206             case ShortcutAction.KeyCode.ENTER:
207                 executeDefaultAction();
208                 break;
209             case ShortcutAction.KeyCode.DELETE:
210                 executeDeleteAction();
211                 break;
212             }
213         });
214     }
215 
216     private void refreshActionbarPreviewImage(Object itemId) {
217         Object previewResource = getPreviewImageForId(itemId);
218         if (previewResource instanceof Resource) {
219             getActionbarPresenter().setPreview((Resource) previewResource);
220         } else {
221             getActionbarPresenter().setPreview(null);
222         }
223     }
224 
225     protected boolean verifyItemExists(Object itemId) {
226         return contentConnector.canHandleItem(itemId) && contentConnector.getItem(itemId) != null;
227     }
228 
229     public List<Object> getSelectedItemIds() {
230         return workbenchPresenter.getSelectedIds();
231     }
232 
233     /**
234      * @return The configured default view Type.<br>
235      * If non define, return the first Content Definition as default.
236      */
237     public String getDefaultViewType() {
238         return workbenchPresenter.getDefaultViewType();
239     }
240 
241     public boolean hasViewType(String viewType) {
242         return workbenchPresenter.hasViewType(viewType);
243     }
244 
245     public BrowserView getView() {
246         return view;
247     }
248 
249     public ActionbarPresenter getActionbarPresenter() {
250         return actionbarPresenter;
251     }
252 
253     /**
254      * Synchronizes the underlying view to reflect the status extracted from the Location token, i.e. selected itemId,
255      * view type and optional query (in case of a search view).
256      */
257     public void resync(final List<Object> itemIds, final String viewType, final String query) {
258         workbenchPresenter.resynch(itemIds, viewType, query);
259     }
260 
261     protected Object getPreviewImageForId(Object itemId) {
262         if (imageProvider != null) {
263             return imageProvider.getThumbnailResource(itemId, ImageProvider.PORTRAIT_GENERATOR);
264         }
265         return null;
266     }
267 
268     @Override
269     public void onActionbarItemClicked(String actionName) {
270         executeAction(actionName);
271     }
272 
273     @Override
274     public void onActionBarSelection(String actionName) {
275         executeAction(actionName);
276     }
277 
278     /**
279      * Executes the default action, as configured in the {@link info.magnolia.ui.actionbar.definition.ActionbarDefinition}.
280      */
281     private void executeDefaultAction() {
282         ActionbarDefinition actionbarDefinition = subAppDescriptor.getActionbar();
283         if (actionbarDefinition == null) {
284             return;
285         }
286         String defaultAction = actionbarDefinition.getDefaultAction();
287         if (StringUtils.isNotEmpty(defaultAction)) {
288             executeAction(defaultAction);
289         }
290     }
291 
292     /**
293      * Executes the default delete action, as configured in the {@link info.magnolia.ui.actionbar.definition.ActionbarDefinition}.
294      */
295     private void executeDeleteAction() {
296         ActionbarDefinition actionbarDefinition = subAppDescriptor.getActionbar();
297         if (actionbarDefinition == null) {
298             return;
299         }
300         String deleteAction = actionbarDefinition.getDeleteAction();
301         if (StringUtils.isNotEmpty(deleteAction)) {
302             executeAction(deleteAction);
303         }
304     }
305 
306     protected void executeAction(String actionName) {
307         try {
308             AvailabilityDefinition availability = actionExecutor.getActionDefinition(actionName).getAvailability();
309             if (availabilityChecker.isAvailable(availability, getSelectedItemIds())) {
310                 Object[] args = prepareActionArgs();
311                 actionExecutor.execute(actionName, args);
312             }
313         } catch (ActionExecutionException e) {
314             Message error = new Message(MessageType.ERROR, "An error occurred while executing an action.", e.getMessage());
315             log.error("An error occurred while executing action [{}]", actionName, e);
316             appContext.sendLocalMessage(error);
317         }
318     }
319 
320     private void executeAction(String actionName, Set<Object> itemIds, Object... parameters) {
321         try {
322             AvailabilityDefinition availability = actionExecutor.getActionDefinition(actionName).getAvailability();
323             if (availabilityChecker.isAvailable(availability, getSelectedItemIds())) {
324                 List<Object> args = new ArrayList<Object>();
325                 args.add(itemIds);
326                 args.addAll(Arrays.asList(parameters));
327                 actionExecutor.execute(actionName, new Object[]{args.toArray(new Object[args.size()])});
328             }
329         } catch (ActionExecutionException e) {
330             Message error = new Message(MessageType.ERROR, "An error occurred while executing an action.", e.getMessage());
331             log.error("An error occurred while executing action [{}]", actionName, e);
332             appContext.sendLocalMessage(error);
333         }
334     }
335 
336     protected Object[] prepareActionArgs() {
337         List<Object> argList = new ArrayList<Object>();
338         List<Item> selectedItems = new ArrayList<Item>();
339 
340         List<Object> selectedIds = getSelectedItemIds();
341         if (selectedIds.isEmpty()) {
342             selectedIds.add(contentConnector.getDefaultItemId());
343         }
344         Iterator<Object> idIt = selectedIds.iterator();
345         while (idIt.hasNext()) {
346             selectedItems.add(contentConnector.getItem(idIt.next()));
347         }
348 
349         if (selectedItems.size() <= 1) {
350             // we have a simple selection; action implementation may expect either an item, either a list parameter, so we have to support both.
351             argList.add(selectedItems.isEmpty() ? new NullItem() : selectedItems.get(0));
352             argList.add(selectedItems);
353         } else {
354             // we have a multiple selection; action implementation must support a list parameter, no way around that.
355             argList.add(selectedItems);
356         }
357         return argList.toArray(new Object[argList.size()]);
358     }
359 }