View Javadoc
1   /**
2    * This file Copyright (c) 2012-2016 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.data.Item;
79  import com.vaadin.event.ShortcutAction;
80  import com.vaadin.server.Resource;
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 = new Predicate<Object>() {
99          @Override
100         public boolean evaluate(final Object object) {
101             return verifyItemExists(object);
102         }
103     };
104 
105     private final WorkbenchPresenter workbenchPresenter;
106 
107     private final ActionExecutor actionExecutor;
108 
109     private final BrowserSubAppDescriptor subAppDescriptor;
110 
111     private final BrowserView view;
112 
113     private final EventBus admincentralEventBus;
114 
115     private final EventBus subAppEventBus;
116 
117     private final ActionbarPresenter actionbarPresenter;
118 
119     private final AvailabilityChecker availabilityChecker;
120 
121     private final AppContext appContext;
122 
123     private final ContentConnector contentConnector;
124 
125     private final ImageProvider imageProvider;
126 
127     @Inject
128     public BrowserPresenter(
129             BrowserView view,
130             SubAppContext subAppContext,
131             ActionExecutor actionExecutor,
132             @Named(AdmincentralEventBus.NAME) EventBus admincentralEventBus,
133             @Named(SubAppEventBus.NAME) EventBus subAppEventBus,
134             ContentConnector contentConnector,
135             ImageProvider imageProvider,
136             WorkbenchPresenter workbenchPresenter,
137             ActionbarPresenter actionbarPresenter,
138             AvailabilityChecker availabilityChecker) {
139         this.view = view;
140         this.appContext = subAppContext.getAppContext();
141         this.subAppDescriptor = (BrowserSubAppDescriptor) subAppContext.getSubAppDescriptor();
142         this.actionExecutor = actionExecutor;
143         this.admincentralEventBus = admincentralEventBus;
144         this.subAppEventBus = subAppEventBus;
145         this.contentConnector = contentConnector;
146         this.imageProvider = imageProvider;
147         this.workbenchPresenter = workbenchPresenter;
148         this.actionbarPresenter = actionbarPresenter;
149         this.availabilityChecker = availabilityChecker;
150     }
151 
152     public BrowserView start() {
153         actionbarPresenter.setListener(this);
154 
155         WorkbenchView workbenchView = workbenchPresenter.start(subAppDescriptor.getWorkbench(), subAppDescriptor.getImageProvider(), subAppEventBus);
156         ActionbarView actionbar = actionbarPresenter.start(subAppDescriptor.getActionbar(), subAppDescriptor.getActions());
157 
158         view.setWorkbenchView(workbenchView);
159         view.setActionbarView(actionbar);
160         view.setListener(this);
161 
162         bindHandlers();
163         return view;
164     }
165 
166     private void bindHandlers() {
167         admincentralEventBus.addHandler(ContentChangedEvent.class, new ContentChangedEvent.Handler() {
168 
169             @Override
170             public void onContentChanged(ContentChangedEvent event) {
171                 List<Object> itemIds = new ArrayList<>();
172                 if (event.getItemId() instanceof Collection) {
173                     for (Object itemId : (Collection<?>) event.getItemId()) {
174                         itemIds.add(itemId);
175                     }
176                 } else {
177                     itemIds.add(event.getItemId());
178                 }
179                 workbenchPresenter.refresh();
180                 // filter out items that can't be handled or doesn't exist
181                 // if an item passed in the event exists, mark it as selected (see MGNLUI-2919)
182                 // otherwise preserve previous selection
183                 List<Object> existingSelectedItemIds = ListUtils.select(itemIds, itemExistsPredicate);
184                 if (existingSelectedItemIds.isEmpty()) {
185                     existingSelectedItemIds = ListUtils.select(getSelectedItemIds(), itemExistsPredicate);
186                 }
187 
188                 workbenchPresenter.select(existingSelectedItemIds);
189 
190                 if (event.isItemContentChanged() && !existingSelectedItemIds.isEmpty()) {
191                     workbenchPresenter.expand(existingSelectedItemIds.get(0));
192                 }
193 
194                 // use just the first selected item to show the preview image
195                 if (!existingSelectedItemIds.isEmpty()) {
196                     refreshActionbarPreviewImage(existingSelectedItemIds.get(0));
197                 }
198             }
199         });
200 
201         subAppEventBus.addHandler(SelectionChangedEvent.class, new SelectionChangedEvent.Handler() {
202 
203             @Override
204             public void onSelectionChanged(SelectionChangedEvent event) {
205                 // if exactly one node is selected, use it for preview
206                 refreshActionbarPreviewImage(event.getFirstItemId());
207             }
208         });
209 
210         subAppEventBus.addHandler(ItemDoubleClickedEvent.class, new ItemDoubleClickedEvent.Handler() {
211 
212             @Override
213             public void onItemDoubleClicked(ItemDoubleClickedEvent event) {
214                 executeDefaultAction();
215             }
216         });
217 
218         subAppEventBus.addHandler(SearchEvent.class, new SearchEvent.Handler() {
219 
220             @Override
221             public void onSearch(SearchEvent event) {
222                 workbenchPresenter.doSearch(event.getSearchExpression());
223             }
224         });
225 
226         subAppEventBus.addHandler(ActionEvent.class, new ActionEvent.Handler() {
227 
228             @Override
229             public void onAction(ActionEvent event) {
230                 executeAction(event.getActionName(), event.getItemIds(), event.getParameters());
231             }
232         });
233 
234         subAppEventBus.addHandler(ItemShortcutKeyEvent.class, new ItemShortcutKeyEvent.Handler() {
235 
236             @Override
237             public void onItemShortcutKeyEvent(ItemShortcutKeyEvent event) {
238                 int keyCode = event.getKeyCode();
239                 switch (keyCode) {
240                 case ShortcutAction.KeyCode.ENTER:
241                     executeDefaultAction();
242                     break;
243                 case ShortcutAction.KeyCode.DELETE:
244                     executeDeleteAction();
245                     break;
246                 }
247 
248             }
249         });
250     }
251 
252     private void refreshActionbarPreviewImage(Object itemId) {
253         Object previewResource = getPreviewImageForId(itemId);
254         if (previewResource instanceof Resource) {
255             getActionbarPresenter().setPreview((Resource) previewResource);
256         } else {
257             getActionbarPresenter().setPreview(null);
258         }
259     }
260 
261     protected boolean verifyItemExists(Object itemId) {
262         return contentConnector.canHandleItem(itemId) && contentConnector.getItem(itemId) != null;
263     }
264 
265     public List<Object> getSelectedItemIds() {
266         return workbenchPresenter.getSelectedIds();
267     }
268 
269     /**
270      * @return The configured default view Type.<br>
271      * If non define, return the first Content Definition as default.
272      */
273     public String getDefaultViewType() {
274         return workbenchPresenter.getDefaultViewType();
275     }
276 
277     public boolean hasViewType(String viewType) {
278         return workbenchPresenter.hasViewType(viewType);
279     }
280 
281     public BrowserView getView() {
282         return view;
283     }
284 
285     public ActionbarPresenter getActionbarPresenter() {
286         return actionbarPresenter;
287     }
288 
289     /**
290      * Synchronizes the underlying view to reflect the status extracted from the Location token, i.e. selected itemId,
291      * view type and optional query (in case of a search view).
292      */
293     public void resync(final List<Object> itemIds, final String viewType, final String query) {
294         workbenchPresenter.resynch(itemIds, viewType, query);
295     }
296 
297     protected Object getPreviewImageForId(Object itemId) {
298         if (imageProvider != null) {
299             return imageProvider.getThumbnailResource(itemId, ImageProvider.PORTRAIT_GENERATOR);
300         }
301         return null;
302     }
303 
304     @Override
305     public void onActionbarItemClicked(String actionName) {
306         executeAction(actionName);
307     }
308 
309     @Override
310     public void onActionBarSelection(String actionName) {
311         executeAction(actionName);
312     }
313 
314     /**
315      * Executes the default action, as configured in the {@link info.magnolia.ui.actionbar.definition.ActionbarDefinition}.
316      */
317     private void executeDefaultAction() {
318         ActionbarDefinition actionbarDefinition = subAppDescriptor.getActionbar();
319         if (actionbarDefinition == null) {
320             return;
321         }
322         String defaultAction = actionbarDefinition.getDefaultAction();
323         if (StringUtils.isNotEmpty(defaultAction)) {
324             executeAction(defaultAction);
325         }
326     }
327 
328     /**
329      * Executes the default delete action, as configured in the {@link info.magnolia.ui.actionbar.definition.ActionbarDefinition}.
330      */
331     private void executeDeleteAction() {
332         ActionbarDefinition actionbarDefinition = subAppDescriptor.getActionbar();
333         if (actionbarDefinition == null) {
334             return;
335         }
336         String deleteAction = actionbarDefinition.getDeleteAction();
337         if (StringUtils.isNotEmpty(deleteAction)) {
338             executeAction(deleteAction);
339         }
340     }
341 
342     private void executeAction(String actionName) {
343         try {
344             AvailabilityDefinition availability = actionExecutor.getActionDefinition(actionName).getAvailability();
345             if (availabilityChecker.isAvailable(availability, getSelectedItemIds())) {
346                 Object[] args = prepareActionArgs();
347                 actionExecutor.execute(actionName, args);
348             }
349         } catch (ActionExecutionException e) {
350             Message error = new Message(MessageType.ERROR, "An error occurred while executing an action.", e.getMessage());
351             log.error("An error occurred while executing action [{}]", actionName, e);
352             appContext.sendLocalMessage(error);
353         }
354     }
355 
356     private void executeAction(String actionName, Set<Object> itemIds, Object... parameters) {
357         try {
358             AvailabilityDefinition availability = actionExecutor.getActionDefinition(actionName).getAvailability();
359             if (availabilityChecker.isAvailable(availability, getSelectedItemIds())) {
360                 List<Object> args = new ArrayList<Object>();
361                 args.add(itemIds);
362                 args.addAll(Arrays.asList(parameters));
363                 actionExecutor.execute(actionName, new Object[] { args.toArray(new Object[args.size()]) });
364             }
365         } catch (ActionExecutionException e) {
366             Message error = new Message(MessageType.ERROR, "An error occurred while executing an action.", e.getMessage());
367             log.error("An error occurred while executing action [{}]", actionName, e);
368             appContext.sendLocalMessage(error);
369         }
370     }
371 
372     protected Object[] prepareActionArgs() {
373         List<Object> argList = new ArrayList<Object>();
374         List<Item> selectedItems = new ArrayList<Item>();
375 
376         List<Object> selectedIds = getSelectedItemIds();
377         if (selectedIds.isEmpty()) {
378             selectedIds.add(contentConnector.getDefaultItemId());
379         }
380         Iterator<Object> idIt = selectedIds.iterator();
381         while (idIt.hasNext()) {
382             selectedItems.add(contentConnector.getItem(idIt.next()));
383         }
384 
385         if (selectedItems.size() <= 1) {
386             // we have a simple selection; action implementation may expect either an item, either a list parameter, so we have to support both.
387             argList.add(selectedItems.isEmpty() ? new NullItem() : selectedItems.get(0));
388             argList.add(selectedItems);
389         } else {
390             // we have a multiple selection; action implementation must support a list parameter, no way around that.
391             argList.add(selectedItems);
392         }
393         return argList.toArray(new Object[argList.size()]);
394     }
395 }