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