View Javadoc
1   /**
2    * This file Copyright (c) 2013-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.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 String workspace;
136     private final AppContext appContext;
137     private final VersionManager versionManager;
138     private final SimpleTranslator i18n;
139 
140     private String caption;
141 
142     @Inject
143     public PagesEditorSubApp(final ActionExecutor actionExecutor, final SubAppContext subAppContext, final PagesEditorSubAppView view, @Named(AdmincentralEventBus.NAME) EventBus admincentralEventBus,
144                              final @Named(SubAppEventBus.NAME) EventBus subAppEventBus, final PageEditorPresenter pageEditorPresenter, final ActionbarPresenter actionbarPresenter, VersionManager versionManager, final SimpleTranslator i18n, AvailabilityChecker availabilityChecker,
145                              ContentConnector contentConnector, StatusBarPresenter statusBar, PageBarPresenter pageBar) {
146         super(subAppContext, view);
147         this.actionExecutor = actionExecutor;
148         this.subAppContext = subAppContext;
149         this.view = view;
150         this.subAppEventBus = subAppEventBus;
151         this.admincentralEventBus = admincentralEventBus;
152         this.pageEditorPresenter = pageEditorPresenter;
153         this.actionbarPresenter = actionbarPresenter;
154         this.availabilityChecker = availabilityChecker;
155         this.statusBar = statusBar;
156         this.pageBar = pageBar;
157         this.contentConnector = (JcrContentConnector) contentConnector;
158         this.workspace = this.contentConnector.getContentConnectorDefinition().getWorkspace();
159         this.appContext = subAppContext.getAppContext();
160         this.versionManager = versionManager;
161         this.i18n = i18n;
162         bindHandlers();
163     }
164 
165     /**
166      * @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)
167      */
168     @Deprecated
169     public PagesEditorSubApp(final ActionExecutor actionExecutor, final SubAppContext subAppContext, final PagesEditorSubAppView view, @Named(AdmincentralEventBus.NAME) EventBus admincentralEventBus,
170             final @Named(SubAppEventBus.NAME) EventBus subAppEventBus, final PageEditorPresenter pageEditorPresenter, final ActionbarPresenter actionbarPresenter, final PageBarView pageBarView,
171             I18NAuthoringSupport i18NAuthoringSupport, I18nContentSupport i18nContentSupport, VersionManager versionManager, final SimpleTranslator i18n, AvailabilityChecker availabilityChecker,
172             ContentConnector contentConnector, StatusBarView statusBarView) {
173         this(actionExecutor, subAppContext, view, admincentralEventBus, subAppEventBus, pageEditorPresenter, actionbarPresenter, versionManager, i18n, availabilityChecker, contentConnector, Components.getComponent(StatusBarPresenter.class), Components.getComponent(PageBarPresenter.class));
174     }
175 
176     @Override
177     public String getCaption() {
178         return caption;
179     }
180 
181     public void updateCaption(DetailLocation location) {
182         this.caption = getPageTitle(location);
183         pageBar.setPageName(caption, location.getNodePath());
184     }
185 
186     @Override
187     public void updateCaptionForExternalPage(String title) {
188         this.caption = title;
189         pageBar.setPageName(caption);
190     }
191 
192     @Override
193     public PagesEditorSubAppView start(Location location) {
194         view.setListener(this);
195 
196         DetailLocation detailLocation = DetailLocation.wrap(location);
197         super.start(detailLocation);
198 
199         ActionbarDefinition actionbarDefinition = ((ContentSubAppDescriptor) getSubAppContext().getSubAppDescriptor()).getActionbar();
200         Map<String, ActionDefinition> actionDefinitions = getSubAppContext().getSubAppDescriptor().getActions();
201         actionbarPresenter.setListener(this);
202         ActionbarView actionbar = actionbarPresenter.start(actionbarDefinition, actionDefinitions);
203         hideActionbarSections();
204 
205         updateCaption(detailLocation);
206 
207         pageEditorPresenter.setListener(this);
208         view.setActionbarView(actionbar);
209         view.setPageBarView(pageBar.start(detailLocation));
210         view.setPageEditorView(pageEditorPresenter.start(detailLocation));
211         view.setStatusBarView(statusBar.start(detailLocation));
212 
213         return view;
214     }
215 
216     /**
217      * Informs the app framework when navigating pages inside the page editor.
218      * Updates the shell fragment, caption and current location.
219      */
220     protected void updateNodePath(String path) {
221         DetailLocation detailLocation = getCurrentLocation();
222         detailLocation.updateNodePath(path);
223         updateLocationDependentComponents(detailLocation);
224         getAppContext().updateSubAppLocation(getSubAppContext(), detailLocation);
225         pageEditorPresenter.getStatus().setNodePath(path);
226         pageEditorPresenter.updateParameters();
227     }
228 
229     @Override
230     public boolean supportsLocation(Location location) {
231         return getCurrentLocation().getNodePath().equals(DetailLocation.wrap(location).getNodePath());
232     }
233 
234     /**
235      * Wraps the current DefaultLocation in a DetailLocation. Providing getter and setters for used parameters.
236      */
237     @Override
238     public DetailLocation getCurrentLocation() {
239         return DetailLocation.wrap(super.getCurrentLocation());
240     }
241 
242     @Override
243     public void deactivateComponents() {
244         updateActionbar();
245         pageBar.deactivateExtensions();
246         statusBar.deactivateExtensions();
247     }
248 
249     @Override
250     public void locationChanged(Location location) {
251         DetailLocation itemLocation = DetailLocation.wrap(location);
252         super.locationChanged(itemLocation);
253 
254         if (pageEditorPresenter.getStatus().isLocationChanged(itemLocation)) {
255             pageEditorPresenter.reload(itemLocation);
256             updateLocationDependentComponents(itemLocation);
257         }
258     }
259 
260     private void updateLocationDependentComponents(DetailLocation location) {
261         pageBar.onLocationUpdate(location);
262         statusBar.onLocationUpdate(location);
263         updateCaption(location);
264     }
265 
266     private String getPageTitle(DetailLocation location) {
267         String caption = StringUtils.EMPTY;
268         try {
269             Session session = MgnlContext.getJCRSession(workspace);
270             Node node = session.getNode(location.getNodePath());
271             if (StringUtils.isNotBlank(location.getVersion())) {
272                 node = versionManager.getVersion(node, location.getVersion());
273                 caption = i18n.translate("subapp.versioned_page", PropertyUtil.getString(node, PROPERTY_TITLE, node.getName()), location.getVersion());
274             } else {
275                 caption = PropertyUtil.getString(node, PROPERTY_TITLE, node.getName());
276             }
277 
278         } catch (RepositoryException e) {
279             log.warn("Could not set page Tab Title for item : {}", location.getNodePath(), e);
280         }
281         return caption;
282     }
283 
284     private void bindHandlers() {
285 
286         admincentralEventBus.addHandler(ContentChangedEvent.class, new ContentChangedEvent.Handler() {
287 
288             @Override
289             public void onContentChanged(ContentChangedEvent event) {
290                 if (contentConnector.canHandleItem(event.getItemId())) {
291                     // Check if the node still exist
292                     try {
293                         String currentNodePath = getCurrentLocation().getNodePath();
294                         if (!MgnlContext.getJCRSession(RepositoryConstants.WEBSITE).nodeExists(currentNodePath)) {
295                             getSubAppContext().close();
296                         }
297                     } catch (RepositoryException e) {
298                         log.warn("Could not determine if currently edited page exists", e);
299                     }
300                 }
301             }
302         });
303 
304         subAppEventBus.addHandler(ContentChangedEvent.class, new ContentChangedEvent.Handler() {
305 
306             @Override
307             public void onContentChanged(ContentChangedEvent event) {
308                 // if locale was changed in the dialog, need to reload pageEditor with url, otherwise just refresh
309                 Locale authoringLocale = subAppContext.getAuthoringLocale();
310                 if (authoringLocale != null && ObjectUtils.notEqual(authoringLocale, pageEditorPresenter.getStatus().getLocale())) {
311                     updateLocationDependentComponents(getCurrentLocation());
312                     // use #loadPageEditor over #refresh to account for potential change of locale
313                     pageEditorPresenter.loadPageEditor();
314                 } else {
315                     pageEditorPresenter.refresh();
316                 }
317             }
318         });
319 
320         subAppEventBus.addHandler(NodeSelectedEvent.class, new NodeSelectedEvent.Handler() {
321 
322             @Override
323             public void onItemSelected(NodeSelectedEvent event) {
324                 AbstractElement element = event.getElement();
325                 if (element instanceof PageElement) {
326                     String path = element.getPath();
327                     if (StringUtils.isEmpty(path)) {
328                         path = "/";
329                     }
330                     if (!path.equals(getCurrentLocation().getNodePath())) {
331                         updateNodePath(path);
332                     }
333                 }
334                 updateActionbar();
335             }
336         });
337 
338     }
339 
340     @Override
341     public void onActionbarItemClicked(String actionName) {
342         prepareAndExecutePagesEditorAction(actionName);
343     }
344 
345     protected void prepareAndExecutePagesEditorAction(String actionName) {
346         AbstractElement selectedElement = pageEditorPresenter.getSelectedElement();
347         if (selectedElement == null) {
348             log.warn("Trying to execute action [{}] but no element was selected. Was the page actually loaded?", actionName);
349             return;
350         }
351         try {
352             Object itemId = getItemId(selectedElement);
353             actionExecutor.execute(actionName, contentConnector.getItem(itemId), selectedElement, pageEditorPresenter);
354         } catch (ActionExecutionException e) {
355             Message error = new Message(MessageType.ERROR, i18n.translate("pages.pagesEditorSubapp.actionExecutionException.message"), e.getMessage());
356             log.error("An error occurred while executing action [{}]", actionName, e);
357             appContext.sendLocalMessage(error);
358         }
359     }
360 
361     public void updateActionbar() {
362         Object itemId = getItemId(pageEditorPresenter.getSelectedElement());
363 
364 
365         // Figure out which section to show, only one
366         List<ActionbarSectionDefinition> sections = getSections();
367         ActionbarSectionDefinition sectionDefinition = getVisibleSection(sections, itemId);
368 
369         // Hide all other sections
370         for (ActionbarSectionDefinition section : sections) {
371             actionbarPresenter.hideSection(section.getName());
372         }
373 
374         if (sectionDefinition != null) {
375             // Show our section
376             actionbarPresenter.showSection(sectionDefinition.getName());
377 
378             // Evaluate availability of each action within the section
379             for (ActionbarGroupDefinition groupDefinition : sectionDefinition.getGroups()) {
380                 for (ActionbarItemDefinition itemDefinition : groupDefinition.getItems()) {
381 
382                     String actionName = itemDefinition.getName();
383                     ActionDefinition actionDefinition = actionExecutor.getActionDefinition(actionName);
384                     if (actionDefinition != null) {
385                         AvailabilityDefinition availability = actionDefinition.getAvailability();
386                         if (availabilityChecker.isAvailable(availability, Arrays.asList(itemId))) {
387                             actionbarPresenter.enable(actionName);
388                         } else {
389                             actionbarPresenter.disable(actionName);
390                         }
391                     }
392                 }
393             }
394         }
395     }
396 
397     private Object getItemId(AbstractElement element) {
398         if (element == null) {
399             return null;
400         }
401         else if (element instanceof AreaElement && ((AreaElement) element).isOptional() && !((AreaElement) element).isCreated()) {
402 
403             try {
404                 int index = element.getPath().lastIndexOf("/");
405                 String parentPath = element.getPath().substring(0, index);
406                 String relPath = element.getPath().substring(index + 1);
407 
408                 Node parent = MgnlContext.getJCRSession(getWorkspace()).getNode(parentPath);
409 
410                 JcrNewNodeItemId jcrNewNodeItemId = new JcrNewNodeItemId(parent.getIdentifier(), getWorkspace(), NodeTypes.Area.NAME);
411                 jcrNewNodeItemId.setName(Path.getUniqueLabel(parent, relPath));
412                 return jcrNewNodeItemId;
413             } catch (RepositoryException e) {
414                 log.error("Failed to create new jcr node item id: " + e.getMessage(), e);
415             }
416             return null;
417         }
418         else {
419             return contentConnector.getItemIdByUrlFragment(element.getPath());
420         }
421     }
422 
423     private List<ActionbarSectionDefinition> getSections() {
424         DetailSubAppDescriptor subAppDescriptor = (DetailSubAppDescriptor) getSubAppContext().getSubAppDescriptor();
425         ActionbarDefinition actionbarDefinition = subAppDescriptor.getActionbar();
426         if (actionbarDefinition == null) {
427             return Collections.emptyList();
428         }
429         return actionbarDefinition.getSections();
430     }
431 
432     private void hideActionbarSections() {
433         for (ActionbarSectionDefinition sectionDefinition : getSections()) {
434             actionbarPresenter.hideSection(sectionDefinition.getName());
435         }
436     }
437 
438     private ActionbarSectionDefinition getVisibleSection(List<ActionbarSectionDefinition> sections, Object itemId) {
439         for (ActionbarSectionDefinition section : sections) {
440             if (availabilityChecker.isAvailable(section.getAvailability(), Arrays.asList(itemId)))
441                 return section;
442         }
443         return null;
444     }
445 
446     @Override
447     public void onEscape() {
448         if (pageEditorPresenter.isMoving()) {
449             pageEditorPresenter.onAction(PageEditorListener.ACTION_CANCEL_MOVE_COMPONENT);
450         } else {
451             // Toggle preview and edit mode.
452             if (getCurrentLocation().getViewType().equals(DetailView.ViewType.EDIT)) {
453                 prepareAndExecutePagesEditorAction(PageEditorListener.ACTION_VIEW_PREVIEW);
454             } else {
455                 prepareAndExecutePagesEditorAction(PageEditorListener.ACTION_VIEW_EDIT);
456             }
457         }
458     }
459 
460     protected String getWorkspace() {
461         return workspace;
462     }
463 
464     @Override
465     public void onMove() {
466         updateActionbar();
467     }
468 
469     @Override
470     public void stop() {
471         pageBar.stop();
472         statusBar.stop();
473     }
474 }