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