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