/*
* (C) Copyright 2006-2007 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Nuxeo - initial API and implementation
*
* $Id: JOOoConvertPluginImpl.java 18651 2007-05-13 20:28:53Z sfermigier $
*/
package org.nuxeo.ecm.webapp.context;
import static org.jboss.seam.ScopeType.CONVERSATION;
import static org.jboss.seam.ScopeType.EVENT;
import static org.jboss.seam.annotations.Install.FRAMEWORK;
import static org.nuxeo.ecm.webapp.helpers.EventNames.NAVIGATE_TO_DOCUMENT;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.seam.Component;
import org.jboss.seam.annotations.Begin;
import org.jboss.seam.annotations.Create;
import org.jboss.seam.annotations.Factory;
import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Install;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.intercept.BypassInterceptors;
import org.jboss.seam.annotations.web.RequestParameter;
import org.jboss.seam.contexts.Context;
import org.jboss.seam.contexts.Contexts;
import org.jboss.seam.core.Events;
import org.nuxeo.common.utils.Path;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentLocation;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.DocumentModelList;
import org.nuxeo.ecm.core.api.DocumentRef;
import org.nuxeo.ecm.core.api.IdRef;
import org.nuxeo.ecm.core.api.NuxeoException;
import org.nuxeo.ecm.core.api.VersionModel;
import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
import org.nuxeo.ecm.core.api.security.SecurityConstants;
import org.nuxeo.ecm.core.schema.FacetNames;
import org.nuxeo.ecm.core.schema.SchemaManager;
import org.nuxeo.ecm.platform.types.Type;
import org.nuxeo.ecm.platform.types.adapter.TypeInfo;
import org.nuxeo.ecm.platform.ui.web.api.NavigationContext;
import org.nuxeo.ecm.platform.ui.web.api.UserAction;
import org.nuxeo.ecm.platform.ui.web.pathelements.ArchivedVersionsPathElement;
import org.nuxeo.ecm.platform.ui.web.pathelements.DocumentPathElement;
import org.nuxeo.ecm.platform.ui.web.pathelements.HiddenDocumentPathElement;
import org.nuxeo.ecm.platform.ui.web.pathelements.PathElement;
import org.nuxeo.ecm.platform.ui.web.pathelements.VersionDocumentPathElement;
import org.nuxeo.ecm.platform.ui.web.util.BadDocumentUriException;
import org.nuxeo.ecm.platform.ui.web.util.DocumentLocator;
import org.nuxeo.ecm.platform.ui.web.util.DocumentsListsUtils;
import org.nuxeo.ecm.platform.util.RepositoryLocation;
import org.nuxeo.ecm.webapp.action.TypesTool;
import org.nuxeo.ecm.webapp.delegate.DocumentManagerBusinessDelegate;
import org.nuxeo.ecm.webapp.helpers.ApplicationControllerHelper;
import org.nuxeo.ecm.webapp.helpers.EventManager;
import org.nuxeo.ecm.webapp.helpers.EventNames;
import org.nuxeo.runtime.api.Framework;
/**
* Implementation for the navigationContext component available on the session.
*/
@Name("navigationContext")
@Scope(CONVERSATION)
@Install(precedence = FRAMEWORK)
public class NavigationContextBean implements NavigationContext, Serializable {
private static final long serialVersionUID = -3708768859028774906L;
private static final Log log = LogFactory.getLog(NavigationContextBean.class);
// --------------------------------------------
// fields managed by this class
// These fields can be accessed by 2 ways
// - simple getters
// - via the context thanks to @Factory
private DocumentModel currentDomain;
private DocumentModel currentContentRoot;
private DocumentModel currentWorkspace;
protected DocumentModel currentDocument;
protected DocumentModel currentSuperSpace;
protected DocumentModelList currentDocumentChildren;
protected List<DocumentModel> currentDocumentParents;
// document model that is not persisted yet (used for creation)
private DocumentModel changeableDocument;
private List<PathElement> parents;
private SchemaManager schemaManager;
@In(create = true, required = false)
protected transient CoreSession documentManager;
@Override
@Create
public void init() {
parents = null;
}
@Override
@BypassInterceptors
public DocumentModel getCurrentDocument() {
return currentDocument;
}
@Override
public String getCurrentDomainPath() {
if (currentDomain != null) {
return currentDomain.getPathAsString();
}
Path path;
if (currentDocument != null) {
path = currentDocument.getPath();
} else {
// Find any document, and lookup its domain.
DocumentModelList docs = documentManager.query("SELECT * FROM Document", 1);
if (docs.size() < 1) {
log.debug("Could not find a single document readable by current user.");
return null;
}
path = docs.get(0).getPath();
}
if (path.segmentCount() > 0) {
String[] segs = { path.segment(0) };
return Path.createFromSegments(segs).toString();
} else {
return null;
}
}
@Override
public void setCurrentDocument(DocumentModel documentModel) {
if (log.isDebugEnabled()) {
log.debug("Setting current document to " + documentModel);
}
if (!checkIfUpdateNeeded(currentDocument, documentModel)) {
if (log.isDebugEnabled()) {
log.debug(String.format("Current document already set to %s => give up updates", documentModel));
}
return;
}
currentSuperSpace = null;
currentDocument = documentModel;
// update all depending variables
updateContextVariables();
resetCurrentPath();
Contexts.getEventContext().remove("currentDocument");
EventManager.raiseEventsOnDocumentSelected(currentDocument);
if (log.isDebugEnabled()) {
log.debug("Current document set to: " + changeableDocument);
}
}
@Override
@BypassInterceptors
public DocumentModel getChangeableDocument() {
return changeableDocument;
}
@Override
public void setChangeableDocument(DocumentModel changeableDocument) {
if (log.isDebugEnabled()) {
log.debug("Setting changeable document to: " + changeableDocument);
}
this.changeableDocument = changeableDocument;
Contexts.getEventContext().set("changeableDocument", changeableDocument);
}
@Override
public DocumentModelList getCurrentPath() {
DocumentModelList parentDocsList = new DocumentModelListImpl();
List<DocumentModel> fromRoot = documentManager.getParentDocuments(currentDocument.getRef());
// add in reverse order
parentDocsList.addAll(fromRoot);
Collections.reverse(parentDocsList);
return parentDocsList;
}
@Override
public DocumentModel getCurrentSuperSpace() {
if (currentSuperSpace == null && currentDocument != null) {
if (currentDocument.hasFacet(FacetNames.SUPER_SPACE)) {
currentSuperSpace = currentDocument;
} else if (documentManager != null) {
currentSuperSpace = documentManager.getSuperSpace(currentDocument);
}
}
return currentSuperSpace;
}
@Override
public void invalidateCurrentDocument() {
if (currentDocument != null) {
currentDocument = documentManager.getDocument(currentDocument.getRef());
updateContextVariables();
}
}
@Override
@BypassInterceptors
public DocumentModel getCurrentDomain() {
return currentDomain;
}
@Override
public void setCurrentDomain(DocumentModel domainDocModel) {
if (!checkIfUpdateNeeded(currentDomain, domainDocModel)) {
return;
}
currentDomain = domainDocModel;
Contexts.getEventContext().remove("currentDomain");
if (domainDocModel == null) {
Events.instance().raiseEvent(EventNames.DOMAIN_SELECTION_CHANGED, currentDomain);
return;
}
if (currentDocument == null) {
setCurrentDocument(null);
}
// if we switched branch then realign currentDocument
if (currentDocumentParents != null
&& !DocumentsListsUtils.isDocumentInList(domainDocModel, currentDocumentParents)) {
setCurrentDocument(domainDocModel);
}
Events.instance().raiseEvent(EventNames.DOMAIN_SELECTION_CHANGED, currentDomain);
}
protected boolean checkIfUpdateNeeded(DocumentModel ctxDoc, DocumentModel newDoc) {
if (log.isDebugEnabled()) {
log.debug(String.format("Check if update needed: compare context " + "doc '%s' to new doc '%s'", ctxDoc,
newDoc));
}
if (ctxDoc == null && newDoc != null || ctxDoc != null && newDoc == null) {
return true;
}
if (ctxDoc == null && newDoc == null) {
return false;
}
if (log.isDebugEnabled()) {
log.debug(String.format(
"Check if update needed: compare cache key on " + "context doc '%s' with new doc '%s'",
ctxDoc.getCacheKey(), newDoc.getCacheKey()));
}
return !ctxDoc.getCacheKey().equals(newDoc.getCacheKey());
}
@Override
public void saveCurrentDocument() {
if (currentDocument == null) {
// cannot call saveDocument with null arg => nasty stateful bean
// de-serialization error
throw new IllegalStateException("null currentDocument");
}
currentDocument = documentManager.saveDocument(currentDocument);
documentManager.save();
}
@Override
public List<PathElement> getCurrentPathList() {
if (parents == null) {
resetCurrentPath();
}
return parents;
}
protected ServerContextBean getServerLocator() {
return (ServerContextBean) Component.getInstance("serverLocator");
}
@Override
public RepositoryLocation getCurrentServerLocation() {
return getServerLocator().getCurrentServerLocation();
}
/**
* @deprecated use getCurrentServerLocation() instead
*/
@Override
@Deprecated
public RepositoryLocation getSelectedServerLocation() {
return getServerLocator().getCurrentServerLocation();
}
/**
* Switches to a new server location by updating the context and updating to the CoreSession (DocumentManager).
*/
@Override
public void setCurrentServerLocation(RepositoryLocation serverLocation) {
if (serverLocation == null) {
log.warn("Setting ServerLocation to null, is this normal ?");
}
RepositoryLocation currentServerLocation = serverLocation;
getServerLocator().setRepositoryLocation(serverLocation);
resetCurrentContext();
Contexts.getEventContext().set("currentServerLocation", currentServerLocation);
// update the documentManager
documentManager = null;
documentManager = getOrCreateDocumentManager();
Events.instance().raiseEvent(EventNames.LOCATION_SELECTION_CHANGED);
DocumentModel rootDocument = documentManager.getRootDocument();
if (documentManager.hasPermission(rootDocument.getRef(), SecurityConstants.READ)) {
currentDocument = rootDocument;
updateContextVariables();
}
}
/**
* Returns the current documentManager if any or create a new session to the current location.
*/
@Override
public CoreSession getOrCreateDocumentManager() {
if (documentManager != null) {
return documentManager;
}
// protect for unexpected wrong cast
Object supposedDocumentManager = Contexts.lookupInStatefulContexts("documentManager");
DocumentManagerBusinessDelegate documentManagerBD = null;
if (supposedDocumentManager != null) {
if (supposedDocumentManager instanceof DocumentManagerBusinessDelegate) {
documentManagerBD = (DocumentManagerBusinessDelegate) supposedDocumentManager;
} else {
log.error("Found the documentManager being " + supposedDocumentManager.getClass()
+ " instead of DocumentManagerBusinessDelegate. This is wrong.");
}
}
if (documentManagerBD == null) {
// this is the first time we select the location, create a
// DocumentManagerBusinessDelegate instance
documentManagerBD = new DocumentManagerBusinessDelegate();
Contexts.getConversationContext().set("documentManager", documentManagerBD);
}
documentManager = documentManagerBD.getDocumentManager(getCurrentServerLocation());
return documentManager;
}
@Override
@BypassInterceptors
public DocumentModel getCurrentWorkspace() {
return currentWorkspace;
}
// Factories to make navigation related data
// available in the context
@Override
@Factory(value = "currentDocument", scope = EVENT)
public DocumentModel factoryCurrentDocument() {
return currentDocument;
}
@Override
@Factory(value = "changeableDocument", scope = EVENT)
public DocumentModel factoryChangeableDocument() {
return changeableDocument;
}
@Override
@Factory(value = "currentDomain", scope = EVENT)
public DocumentModel factoryCurrentDomain() {
return currentDomain;
}
@Override
@Factory(value = "currentWorkspace", scope = EVENT)
public DocumentModel factoryCurrentWorkspace() {
return currentWorkspace;
}
@Override
@Factory(value = "currentContentRoot", scope = EVENT)
public DocumentModel factoryCurrentContentRoot() {
return currentContentRoot;
}
// @Factory(value = "currentServerLocation", scope = EVENT)
@Override
public RepositoryLocation factoryCurrentServerLocation() {
return getCurrentServerLocation();
}
@Override
@Factory(value = "currentSuperSpace", scope = EVENT)
public DocumentModel factoryCurrentSuperSpace() {
return getCurrentSuperSpace();
}
public void setCurrentWorkspace(DocumentModel workspaceDocModel) {
if (!checkIfUpdateNeeded(currentWorkspace, workspaceDocModel)) {
return;
}
currentWorkspace = workspaceDocModel;
if (workspaceDocModel == null) {
return;
}
if (currentDocument == null) {
setCurrentDocument(workspaceDocModel);
return;
}
// if we switched branch then realign currentDocument
if (currentDocumentParents != null
&& !DocumentsListsUtils.isDocumentInList(workspaceDocModel, currentDocumentParents)) {
setCurrentDocument(workspaceDocModel);
return;
}
}
@Override
public void updateDocumentContext(DocumentModel doc) {
setCurrentDocument(doc);
}
/**
* Updates variables according to hierarchy rules and to the new currentDocument.
*/
protected void updateContextVariables() {
// XXX flush method is not implemented for Event context :)
Contexts.getEventContext().set("currentDocument", currentDocument);
// Don't flush changeable document with a null id (NXP-10732)
if ((getChangeableDocument() != null) && (getChangeableDocument().getId() != null)) {
setChangeableDocument(null);
}
if (currentDocument == null) {
currentDocumentParents = null;
return;
}
DocumentRef ref = currentDocument.getRef();
if (ref == null) {
throw new NuxeoException("DocumentRef is null for currentDocument: " + currentDocument.getName());
}
// Recompute document parents
currentDocumentParents = documentManager.getParentDocuments(ref);
// iterate in reverse list order to go down the tree
// set all navigation variables according to docType
// => update to tree
String docType;
if (currentDocumentParents != null) {
for (int i = currentDocumentParents.size() - 1; i >= 0; i--) {
DocumentModel docModel = currentDocumentParents.get(i);
docType = docModel.getType();
if (docType != null && hasSuperType(docType, "Workspace")) {
setCurrentWorkspace(docModel);
}
if (docType == null || hasSuperType(docType, "WorkspaceRoot") || hasSuperType(docType, "SectionRoot")) {
setCurrentContentRoot(docModel);
}
if (docType != null && hasSuperType(docType, "Domain")) {
setCurrentDomain(docModel);
}
}
}
// reinit lower tree
docType = currentDocument.getType();
if (docType.equals("Root")) {
setCurrentDomain(null);
setCurrentContentRoot(null);
setCurrentWorkspace(null);
} else if (hasSuperType(docType, "Domain")) {
setCurrentDomain(currentDocument);
setCurrentContentRoot(null);
setCurrentWorkspace(null);
} else if (hasSuperType(docType, "WorkspaceRoot") || hasSuperType(docType, "SectionRoot")) {
setCurrentContentRoot(currentDocument);
setCurrentWorkspace(null);
} else if (hasSuperType(docType, "Workspace")) {
setCurrentWorkspace(currentDocument);
}
// lazily recompute some fields
currentSuperSpace = null;
parents = null;
}
private boolean hasSuperType(String targetDocType, String superType) {
SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
return schemaManager.hasSuperType(targetDocType, superType);
}
@Override
public void resetCurrentContext() {
// flush event context
Context eventContext = Contexts.getEventContext();
eventContext.remove("currentDocument");
eventContext.remove("changeableDocument");
eventContext.remove("currentDocumentChildren");
eventContext.remove("currentDomain");
eventContext.remove("currentServerLocation");
eventContext.remove("currentWorkspace");
eventContext.remove("currentContentRoot");
eventContext.remove("currentSuperSpace");
}
// XXX AT: we should let each action listener raise specific events
// (edition) and decide what's the next view, let's just handle context
// setting and redirection + this should be callable from templates i.e use
// view as a string.
@Override
public String getActionResult(DocumentModel doc, UserAction action) {
TypesTool typesTool = (TypesTool) Component.getInstance("typesTool");
if (doc == null) {
return null;
}
if (UserAction.CREATE == action) {
// the given document is a changeable document
setChangeableDocument(doc);
} else {
updateDocumentContext(doc);
}
final Type type = typesTool.getType(doc.getType());
final String result;
if (UserAction.VIEW == action) {
assert currentDocument != null;
EventManager.raiseEventsOnDocumentSelected(currentDocument);
result = ApplicationControllerHelper.getPageOnSelectedDocumentType(type);
} else if (UserAction.EDIT == action) {
throw new UnsupportedOperationException("for action " + action);
} else if (UserAction.AFTER_EDIT == action) {
assert currentDocument != null;
EventManager.raiseEventsOnDocumentChange(currentDocument);
result = ApplicationControllerHelper.getPageOnEditedDocumentType(type);
} else if (UserAction.CREATE == action) {
EventManager.raiseEventsOnDocumentCreate(changeableDocument);
result = ApplicationControllerHelper.getPageOnCreateDocumentType(type);
} else if (UserAction.AFTER_CREATE == action) {
assert currentDocument != null;
EventManager.raiseEventsOnDocumentSelected(currentDocument);
result = ApplicationControllerHelper.getPageOnCreatedDocumentType(type);
} else if (UserAction.GO_HOME == action) {
EventManager.raiseEventsOnGoingHome();
result = "home";
} else {
log.error(String.format("Unknown action '%s' for navigation on " + "document '%s' with title '%s': ",
action.name(), doc.getId(), doc.getTitle()));
result = null;
}
return result;
}
@Override
public String goHome() {
resetCurrentContext();
EventManager.raiseEventsOnGoingHome();
return "home";
}
@Override
public String goBack() {
if (currentDocument != null) {
setChangeableDocument(null);
return navigateToDocument(currentDocument);
} else {
return goHome();
}
}
@Override
public String navigateToId(String documentId) {
if (documentManager == null) {
throw new IllegalStateException("documentManager not initialized");
}
DocumentRef docRef = new IdRef(documentId);
final DocumentModel doc = documentManager.getDocument(docRef);
return navigateToDocument(doc, "view");
}
@Override
public String navigateToRef(DocumentRef docRef) {
if (documentManager == null) {
throw new IllegalStateException("documentManager not initialized");
}
final DocumentModel doc = documentManager.getDocument(docRef);
return navigateToDocument(doc, "view");
}
@Override
public String navigateToDocument(DocumentModel doc) {
return navigateToDocument(doc, "view");
}
@Override
public String navigateToDocument(DocumentModel doc, String viewId) {
if (doc != null) {
updateDocumentContext(doc);
}
assert currentDocument != null;
TypeInfo typeInfo = currentDocument.getAdapter(TypeInfo.class);
String chosenView = null;
if (typeInfo != null) {
String defaultView = typeInfo.getDefaultView();
// hardcoded default views
if ("view".equals(viewId)) {
chosenView = defaultView;
} else if ("create".equals(viewId)) {
chosenView = typeInfo.getCreateView();
} else if ("edit".equals(viewId)) {
chosenView = typeInfo.getEditView();
} else {
chosenView = typeInfo.getView(viewId);
}
if (chosenView == null) {
chosenView = defaultView;
}
}
Events.instance().raiseEvent(NAVIGATE_TO_DOCUMENT, currentDocument);
return chosenView;
}
@Override
public String navigateToDocumentWithView(DocumentModel doc, String viewId) {
return navigateToDocument(doc, viewId);
}
@Override
public String navigateToDocument(DocumentModel docModel, VersionModel versionModel) {
DocumentModel docVersion = documentManager.getDocumentWithVersion(docModel.getRef(), versionModel);
return navigateToDocument(docVersion);
}
@Override
public void selectionChanged() {
resetCurrentPath();
}
@Override
public String getCurrentDocumentUrl() {
if (currentDocument == null) {
log.error("current document is null");
return null;
}
return DocumentLocator.getDocumentUrl(getCurrentServerLocation(), currentDocument.getRef());
}
@Override
public String getCurrentDocumentFullUrl() {
if (currentDocument == null) {
log.error("current document is null");
return null;
}
return DocumentLocator.getFullDocumentUrl(getCurrentServerLocation(), currentDocument.getRef());
}
// start a new conversation if needed, join main if possible
@Override
@Begin(id = "#{conversationIdGenerator.currentOrNewMainConversationId}", join = true)
public String navigateTo(RepositoryLocation serverLocation, DocumentRef docRef) {
// re-connect only if there is another repository specified
if (!serverLocation.equals(getCurrentServerLocation())) {
setCurrentServerLocation(serverLocation);
}
return navigateToRef(docRef);
}
// start a new conversation if needed, join main if possible
@Override
@Begin(id = "#{conversationIdGenerator.currentOrNewMainConversationId}", join = true)
public String navigateToURL(String documentUrl) {
final DocumentLocation docLoc;
try {
docLoc = DocumentLocator.parseDocRef(documentUrl);
} catch (BadDocumentUriException e) {
log.error("Cannot get document ref from uri " + documentUrl + ". " + e.getMessage(), e);
return null;
}
final DocumentRef docRef = docLoc.getDocRef();
RepositoryLocation repLoc = new RepositoryLocation(docLoc.getServerName());
return navigateTo(repLoc, docRef);
}
@RequestParameter
String docRef;
/**
* @see NavigationContext#navigateToURL()
*/
@Override
@Begin(id = "#{conversationIdGenerator.currentOrNewMainConversationId}", join = true)
public String navigateToURL() {
if (docRef == null) {
return null;
}
return navigateToURL(docRef);
}
protected void resetCurrentPath() {
final String logPrefix = "<resetCurrentPath> ";
parents = new ArrayList<PathElement>();
if (documentManager == null) {
log.error(logPrefix + "documentManager not initialized");
return;
}
if (currentDocument != null) {
if (currentDocument.isVersion()) {
DocumentModel sourceDocument = documentManager.getSourceDocument(currentDocument.getRef());
List<DocumentModel> parentList = documentManager.getParentDocuments(sourceDocument.getRef());
for (DocumentModel docModel : parentList) {
parents.add(getDocumentPathElement(docModel));
}
parents.add(new ArchivedVersionsPathElement(sourceDocument));
parents.add(new VersionDocumentPathElement(currentDocument));
} else {
if (currentDocumentParents != null) {
for (DocumentModel docModel : currentDocumentParents) {
parents.add(getDocumentPathElement(docModel));
}
}
}
}
}
protected PathElement getDocumentPathElement(DocumentModel doc) {
if (doc != null && doc.hasFacet(FacetNames.HIDDEN_IN_NAVIGATION)) {
return new HiddenDocumentPathElement(doc);
}
return new DocumentPathElement(doc);
}
@Override
public DocumentModel getCurrentContentRoot() {
return currentContentRoot;
}
@Override
public void setCurrentContentRoot(DocumentModel crDocumentModel) {
if (!checkIfUpdateNeeded(currentContentRoot, crDocumentModel)) {
return;
}
currentContentRoot = crDocumentModel;
Contexts.getEventContext().remove("currentContentRoot");
if (crDocumentModel == null) {
return;
}
if (currentDocument == null) {
setCurrentDocument(null);
return;
}
// if we switched branch then realign currentDocument
if (currentDocumentParents != null
&& !DocumentsListsUtils.isDocumentInList(crDocumentModel, currentDocumentParents)) {
setCurrentDocument(crDocumentModel);
}
}
}