View Javadoc
1   /**
2    * This file Copyright (c) 2013-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.pages.app.editor;
35  
36  import info.magnolia.event.EventBus;
37  import info.magnolia.i18nsystem.SimpleTranslator;
38  import info.magnolia.pages.app.editor.event.NodeSelectedEvent;
39  import info.magnolia.pages.app.editor.pagebar.PageBarPresenter;
40  import info.magnolia.pages.app.editor.statusbar.StatusBarPresenter;
41  import info.magnolia.ui.actionbar.ActionbarPresenter;
42  import info.magnolia.ui.actionbar.ActionbarView;
43  import info.magnolia.ui.actionbar.definition.ActionbarDefinition;
44  import info.magnolia.ui.actionbar.definition.ActionbarGroupDefinition;
45  import info.magnolia.ui.actionbar.definition.ActionbarItemDefinition;
46  import info.magnolia.ui.actionbar.definition.ActionbarSectionDefinition;
47  import info.magnolia.ui.api.action.ActionDefinition;
48  import info.magnolia.ui.api.action.ActionExecutionException;
49  import info.magnolia.ui.api.action.ActionExecutor;
50  import info.magnolia.ui.api.app.AppContext;
51  import info.magnolia.ui.api.app.SubAppContext;
52  import info.magnolia.ui.api.app.SubAppEventBus;
53  import info.magnolia.ui.api.availability.AvailabilityChecker;
54  import info.magnolia.ui.api.availability.AvailabilityDefinition;
55  import info.magnolia.ui.api.event.AdmincentralEventBus;
56  import info.magnolia.ui.api.event.ContentChangedEvent;
57  import info.magnolia.ui.api.location.Location;
58  import info.magnolia.ui.api.message.Message;
59  import info.magnolia.ui.api.message.MessageType;
60  import info.magnolia.ui.contentapp.definition.ContentSubAppDescriptor;
61  import info.magnolia.ui.contentapp.detail.DetailLocation;
62  import info.magnolia.ui.contentapp.detail.DetailSubAppDescriptor;
63  import info.magnolia.ui.contentapp.detail.DetailView;
64  import info.magnolia.ui.framework.app.BaseSubApp;
65  import info.magnolia.ui.framework.ioc.AdmincentralFlavour;
66  import info.magnolia.ui.vaadin.editor.PageEditorListener;
67  import info.magnolia.ui.vaadin.gwt.client.shared.AbstractElement;
68  import info.magnolia.ui.vaadin.gwt.client.shared.PageElement;
69  
70  import java.util.Arrays;
71  import java.util.Collections;
72  import java.util.List;
73  import java.util.Locale;
74  import java.util.Map;
75  
76  import javax.inject.Inject;
77  import javax.inject.Named;
78  
79  import org.apache.commons.lang3.ObjectUtils;
80  import org.apache.commons.lang3.StringUtils;
81  import org.slf4j.Logger;
82  import org.slf4j.LoggerFactory;
83  
84  /**
85   * SubApp putting together:
86   * <ul>
87   *   <li>{@link PageEditorPresenter}</li>
88   *   <li>{@link ActionbarPresenter}</li>
89   *   <li>{@link StatusBarPresenter}</li>
90   *   <li>{@link PageBarPresenter}</li>
91   * </ul>
92   * Keeps track on changes coming from the client side of the {@link PageEditorPresenter} by registering a {@link NodeSelectedEvent.Handler}
93   * to the subApp eventbus. This is triggered every time the {@link PageElement}, {@link info.magnolia.ui.vaadin.gwt.client.shared.AreaElement}
94   * or {@link info.magnolia.ui.vaadin.gwt.client.shared.ComponentElement} is selected inside the iFrame. This will result in updating the
95   * {@link ActionbarPresenter} accordingly using action availability. <br />
96   *
97   * When a page change happens, e.g. the user browses inside the iFrame, this will also be detected and trigger a chain
98   * of actions taking care of updating all necessary parts of the UI as well as the underlying framework.
99   *
100  */
101 public class PagesEditorSubApp extends BaseSubApp<PagesEditorSubAppView> implements PagesEditorSubAppView.Listener, ActionbarPresenter.Listener, PageEditorPresenter.Listener {
102 
103     private static final Logger log = LoggerFactory.getLogger(PagesEditorSubApp.class);
104 
105     private final ActionExecutor actionExecutor;
106     private final SubAppContext subAppContext;
107     private final PagesEditorSubAppView view;
108     private final EventBus subAppEventBus;
109     private final EventBus admincentralEventBus;
110     private final PageEditorPresenter pageEditorPresenter;
111     private final ActionbarPresenter actionbarPresenter;
112     private final PagesContentConnector contentConnector;
113     private final AvailabilityChecker availabilityChecker;
114     private final StatusBarPresenter statusBar;
115     private final PageBarPresenter pageBar;
116     private final AppContext appContext;
117     private final SimpleTranslator i18n;
118 
119     private String caption;
120 
121     @Inject
122     public PagesEditorSubApp(final ActionExecutor actionExecutor, final SubAppContext subAppContext, final PagesEditorSubAppView view,
123                              final @Named(AdmincentralEventBus.NAME) EventBus admincentralEventBus, final @Named(SubAppEventBus.NAME) EventBus subAppEventBus,
124                              final PageEditorPresenter pageEditorPresenter, final ActionbarPresenter actionbarPresenter,
125                              final SimpleTranslator i18n, AvailabilityChecker availabilityChecker, final PagesContentConnector contentConnector,
126                              final StatusBarPresenter statusBar, final PageBarPresenter pageBar) {
127         super(subAppContext, view);
128         this.actionExecutor = actionExecutor;
129         this.subAppContext = subAppContext;
130         this.view = view;
131         this.subAppEventBus = subAppEventBus;
132         this.admincentralEventBus = admincentralEventBus;
133         this.pageEditorPresenter = pageEditorPresenter;
134         this.actionbarPresenter = actionbarPresenter;
135         this.availabilityChecker = availabilityChecker;
136         this.statusBar = statusBar;
137         this.pageBar = pageBar;
138         this.contentConnector = contentConnector;
139         this.appContext = subAppContext.getAppContext();
140         this.i18n = i18n;
141         bindHandlers();
142     }
143 
144     @Override
145     public String getCaption() {
146         return caption;
147     }
148 
149     public void updateCaption(DetailLocation location) {
150         this.caption = contentConnector.getPageTitle(location.getNodePath(), location.getVersion());
151         if (AdmincentralFlavour.get().isM5()) {
152             pageBar.setPageName(caption, location.getNodePath());
153         } else {
154             statusBar.setPageName(caption, location.getNodePath());
155         }
156     }
157 
158     @Override
159     public void updateCaptionForExternalPage(String title) {
160         this.caption = title;
161         if (AdmincentralFlavour.get().isM5()) {
162             pageBar.setPageName(caption);
163         } else {
164             statusBar.setPageName(caption);
165         }
166     }
167 
168     @Override
169     public PagesEditorSubAppView start(Location location) {
170         view.setListener(this);
171 
172         DetailLocation detailLocation = DetailLocation.wrap(location);
173         super.start(detailLocation);
174 
175         ActionbarDefinition actionbarDefinition = ((ContentSubAppDescriptor) getSubAppContext().getSubAppDescriptor()).getActionbar();
176         Map<String, ActionDefinition> actionDefinitions = getSubAppContext().getSubAppDescriptor().getActions();
177         actionbarPresenter.setListener(this);
178         ActionbarView actionbar = actionbarPresenter.start(actionbarDefinition, actionDefinitions);
179         hideActionbarSections();
180 
181         updateCaption(detailLocation);
182 
183         pageEditorPresenter.setListener(this);
184         view.setActionbarView(actionbar);
185         if (AdmincentralFlavour.get().isM5()) {
186             view.setPageBarView(pageBar.start(detailLocation));
187         }
188         // initialize status-bar before page-editor, current locale is initialized from the language-selector extension
189         view.setStatusBarView(statusBar.start(detailLocation));
190         view.setPageEditorView(pageEditorPresenter.start(detailLocation));
191 
192         return view;
193     }
194 
195     /**
196      * Informs the app framework when navigating pages inside the page editor.
197      * Updates the shell fragment, caption and current location.
198      */
199     protected void updateNodePath(String path) {
200         DetailLocation detailLocation = getCurrentLocation();
201         detailLocation.updateNodePath(path);
202         updateLocationDependentComponents(detailLocation);
203         getAppContext().updateSubAppLocation(getSubAppContext(), detailLocation);
204         pageEditorPresenter.getStatus().setNodePath(path);
205         pageEditorPresenter.updateParameters();
206     }
207 
208     @Override
209     public boolean supportsLocation(Location location) {
210         return getCurrentLocation().getNodePath().equals(DetailLocation.wrap(location).getNodePath());
211     }
212 
213     /**
214      * Wraps the current DefaultLocation in a DetailLocation. Providing getter and setters for used parameters.
215      */
216     @Override
217     public DetailLocation getCurrentLocation() {
218         return DetailLocation.wrap(super.getCurrentLocation());
219     }
220 
221     @Override
222     public void deactivateComponents() {
223         updateActionbar();
224         pageBar.deactivateExtensions();
225         statusBar.deactivateExtensions();
226     }
227 
228     @Override
229     public void locationChanged(Location location) {
230         DetailLocation itemLocation = DetailLocation.wrap(location);
231         super.locationChanged(itemLocation);
232 
233         if (pageEditorPresenter.getStatus().isLocationChanged(itemLocation)) {
234             pageEditorPresenter.reload(itemLocation);
235             updateLocationDependentComponents(itemLocation);
236         }
237     }
238 
239     private void updateLocationDependentComponents(DetailLocation location) {
240         pageBar.onLocationUpdate(location);
241         statusBar.onLocationUpdate(location);
242         updateCaption(location);
243     }
244 
245     private void bindHandlers() {
246 
247         admincentralEventBus.addHandler(ContentChangedEvent.class, new ContentChangedEvent.Handler() {
248 
249             @Override
250             public void onContentChanged(ContentChangedEvent event) {
251                 if (contentConnector.canHandleItem(event.getItemId())) {
252                     if (!contentConnector.itemExists(event.getItemId())) {
253                         getSubAppContext().close();
254                     }
255                 }
256             }
257         });
258 
259         subAppEventBus.addHandler(ContentChangedEvent.class, new ContentChangedEvent.Handler() {
260 
261             @Override
262             public void onContentChanged(ContentChangedEvent event) {
263                 // Updating the shared state.
264                 AbstractElement element = contentConnector.getElementFrom(event.getItemId());
265                 pageEditorPresenter.getStatus().setSelectedElement(element);
266 
267                 // Update the parameters for syncing the shared state.
268                 pageEditorPresenter.updateParameters();
269 
270                 // if locale was changed in the dialog, need to reload pageEditor with url, otherwise just refresh
271                 Locale authoringLocale = subAppContext.getAuthoringLocale();
272                 if (authoringLocale != null && ObjectUtils.notEqual(authoringLocale, pageEditorPresenter.getStatus().getLocale())) {
273                     updateLocationDependentComponents(getCurrentLocation());
274                     // use #loadPageEditor over #refresh to account for potential change of locale
275                     pageEditorPresenter.loadPageEditor();
276                 } else {
277                     pageEditorPresenter.refresh();
278                 }
279             }
280         });
281 
282         subAppEventBus.addHandler(NodeSelectedEvent.class, new NodeSelectedEvent.Handler() {
283 
284             @Override
285             public void onItemSelected(NodeSelectedEvent event) {
286                 AbstractElement element = event.getElement();
287                 if (element instanceof PageElement) {
288                     String path = element.getPath();
289                     if (StringUtils.isEmpty(path)) {
290                         path = "/";
291                     }
292                     if (!path.equals(getCurrentLocation().getNodePath())) {
293                         updateNodePath(path);
294                     }
295                 }
296                 updateActionbar();
297             }
298         });
299 
300     }
301 
302     @Override
303     public void onActionbarItemClicked(String actionName) {
304          prepareAndExecutePagesEditorAction(actionName);
305     }
306 
307     /**
308      * Used for executing actions from the actionbar. So 'server-side' actions which are depending on a selected element
309      * in the page editor.
310      *
311      * @see PageEditorListener#onAction(String, Object...) for 'client-side' action execution as comparison.
312      */
313     protected void prepareAndExecutePagesEditorAction(String actionName) {
314         AbstractElement selectedElement = pageEditorPresenter.getSelectedElement();
315         if (selectedElement == null) {
316             log.warn("Trying to execute action [{}] but no element was selected. Was the page actually loaded?", actionName);
317             return;
318         }
319         try {
320             actionExecutor.execute(actionName, selectedElement, pageEditorPresenter);
321         } catch (ActionExecutionException e) {
322             Message error = new Message(MessageType.ERROR, i18n.translate("pages.pagesEditorSubapp.actionExecutionException.message"), e.getMessage());
323             log.error("An error occurred while executing action [{}]", actionName, e);
324             appContext.sendLocalMessage(error);
325         }
326     }
327 
328     public void updateActionbar() {
329         Object itemId = contentConnector.getItemIdByElement(pageEditorPresenter.getSelectedElement());
330 
331         // Figure out which section to show, only one
332         List<ActionbarSectionDefinition> sections = getSections();
333         ActionbarSectionDefinition sectionDefinition = getVisibleSection(sections, itemId);
334 
335         // Hide all other sections
336         for (ActionbarSectionDefinition section : sections) {
337             actionbarPresenter.hideSection(section.getName());
338         }
339 
340         if (sectionDefinition != null) {
341             // Show our section
342             actionbarPresenter.showSection(sectionDefinition.getName());
343 
344             // Evaluate availability of each action within the section
345             for (ActionbarGroupDefinition groupDefinition : sectionDefinition.getGroups()) {
346                 for (ActionbarItemDefinition itemDefinition : groupDefinition.getItems()) {
347 
348                     String actionName = itemDefinition.getName();
349                     ActionDefinition actionDefinition = actionExecutor.getActionDefinition(actionName);
350                     if (actionDefinition != null) {
351                         AvailabilityDefinition availability = actionDefinition.getAvailability();
352                         if (availabilityChecker.isAvailable(availability, Arrays.asList(itemId))) {
353                             actionbarPresenter.enable(actionName);
354                         } else {
355                             actionbarPresenter.disable(actionName);
356                         }
357                     }
358                 }
359             }
360         }
361     }
362 
363     private List<ActionbarSectionDefinition> getSections() {
364         DetailSubAppDescriptor subAppDescriptor = (DetailSubAppDescriptor) getSubAppContext().getSubAppDescriptor();
365         ActionbarDefinition actionbarDefinition = subAppDescriptor.getActionbar();
366         if (actionbarDefinition == null) {
367             return Collections.emptyList();
368         }
369         return actionbarDefinition.getSections();
370     }
371 
372     private void hideActionbarSections() {
373         for (ActionbarSectionDefinition sectionDefinition : getSections()) {
374             actionbarPresenter.hideSection(sectionDefinition.getName());
375         }
376     }
377 
378     private ActionbarSectionDefinition getVisibleSection(List<ActionbarSectionDefinition> sections, Object itemId) {
379         for (ActionbarSectionDefinition section : sections) {
380             if (availabilityChecker.isAvailable(section.getAvailability(), Arrays.asList(itemId)))
381                 return section;
382         }
383         return null;
384     }
385 
386     @Override
387     public void onEscape() {
388         if (pageEditorPresenter.isMoving()) {
389             pageEditorPresenter.onAction(PageEditorListener.ACTION_CANCEL_MOVE_COMPONENT);
390         } else {
391             // Toggle preview and edit mode.
392             if (getCurrentLocation().getViewType().equals(DetailView.ViewType.EDIT)) {
393                 prepareAndExecutePagesEditorAction(PageEditorListener.ACTION_VIEW_PREVIEW);
394             } else {
395                 prepareAndExecutePagesEditorAction(PageEditorListener.ACTION_VIEW_EDIT);
396             }
397         }
398     }
399 
400     @Override
401     public String getIcon(Location location) {
402         DetailLocation detailLocation = DetailLocation.wrap(location);
403         return detailLocation.getViewType().getIcon();
404     }
405 
406     @Override
407     public void onMove() {
408         updateActionbar();
409     }
410 
411     @Override
412     public void stop() {
413         pageBar.stop();
414         statusBar.stop();
415     }
416 }