/*
* (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: LiveEditBootstrapHelper.java 30586 2008-02-26 14:30:17Z ogrisel $
*/
package org.nuxeo.ecm.webapp.liveedit;
import static org.jboss.seam.ScopeType.EVENT;
import java.io.IOException;
import java.io.Serializable;
import java.util.Calendar;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.faces.context.FacesContext;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dom4j.Document;
import org.dom4j.DocumentFactory;
import org.dom4j.Element;
import org.dom4j.QName;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Factory;
import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.web.RequestParameter;
import org.jboss.seam.annotations.Scope;
import org.nuxeo.ecm.core.api.Blob;
import org.nuxeo.ecm.core.api.CoreInstance;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.DocumentNotFoundException;
import org.nuxeo.ecm.core.api.IdRef;
import org.nuxeo.ecm.core.api.LifeCycleConstants;
import org.nuxeo.ecm.core.api.NuxeoException;
import org.nuxeo.ecm.core.api.PropertyException;
import org.nuxeo.ecm.core.api.security.SecurityConstants;
import org.nuxeo.ecm.core.schema.FacetNames;
import org.nuxeo.ecm.platform.mimetype.interfaces.MimetypeEntry;
import org.nuxeo.ecm.platform.mimetype.interfaces.MimetypeRegistry;
import org.nuxeo.ecm.platform.ui.web.api.NavigationContext;
import org.nuxeo.ecm.platform.ui.web.tag.fn.LiveEditConstants;
import org.nuxeo.ecm.platform.ui.web.util.BaseURL;
import org.nuxeo.runtime.api.Framework;
/**
* The LiveEdit bootstrap procedure works as follows:
* <ul>
* <li>browsed page calls a JSF function from the DocumentModelFunctions class (edit a document, create new document,
* etc.) to generate;</li>
* <li>composing a specific URL as result, triggering the bootstrap addon to popup;</li>
* <li>the addon come back with the URL composed allowing the present seam component to create the bootstrap file. The
* file contains various data as requested in the URL;</li>
* <li>the XML file is now available to addon which presents it to the client plugin.</li>
* </ul>
* Please refer to the nuxeo book chapter on desktop integration for details on the format of the nxedit URLs and the
* XML bootstrap file.
*
* @author Thierry Delprat NXP-1959 the bootstrap file is managing the 'create new document [from template]' case too.
* The URL is containing an action identifier.
* @author Rux rdarlea@nuxeo.com
* @author Olivier Grisel ogrisel@nuxeo.com (split url functions into JSF DocumentModelFunctions module)
*/
@Scope(EVENT)
@Name("liveEditHelper")
public class LiveEditBootstrapHelper implements Serializable, LiveEditConstants {
protected static final String MODIFIED_FIELD = "modified";
protected static final String DUBLINCORE_SCHEMA = "dublincore";
private static final Log log = LogFactory.getLog(LiveEditBootstrapHelper.class);
private static final long serialVersionUID = 876879071L;
@In(create = true)
protected transient NavigationContext navigationContext;
@In(create = true, required = false)
protected transient CoreSession documentManager;
@RequestParameter
protected String action;
@RequestParameter
protected String repoID;
@RequestParameter
protected String templateRepoID;
@RequestParameter
protected String docRef;
@RequestParameter
protected String templateDocRef;
@In(create = true)
protected LiveEditClientConfig liveEditClientConfig;
/**
* @deprecated use blobPropertyField and filenamePropertyField instead
*/
@Deprecated
@RequestParameter
protected String schema;
@RequestParameter
protected String templateSchema;
/**
* @deprecated use blobPropertyField instead
*/
@Deprecated
@RequestParameter
protected String blobField;
@RequestParameter
protected String blobPropertyName;
@RequestParameter
protected String templateBlobField;
// TODO: to be deprecated once all filenames are stored in the blob itself
/**
* @deprecated use filenamePropertyField instead
*/
@Deprecated
@RequestParameter
protected String filenameField;
// TODO: to be deprecated once all filenames are stored in the blob itself
@RequestParameter
protected String filenamePropertyName;
@RequestParameter
protected String mimetype;
@RequestParameter
protected String docType;
protected MimetypeRegistry mimetypeRegistry;
// Event-long cache for mimetype lookups - no invalidation required
protected final Map<String, Boolean> cachedEditableStates = new HashMap<String, Boolean>();
// Event-long cache for document field lookups - no invalidation required
protected final Map<String, Boolean> cachedEditableBlobs = new HashMap<String, Boolean>();
/**
* Creates the bootstrap file. It is called from the browser's addon. The URL composition tells the case and what to
* create. The structure is depicted in the NXP-1881. Rux NXP-1959: add new tag on root level describing the action:
* actionEdit, actionNew or actionFromTemplate.
*
* @return the bootstrap file content
*/
public void getBootstrap() throws IOException {
String currentRepoID = documentManager.getRepositoryName();
CoreSession session = documentManager;
CoreSession templateSession = documentManager;
try {
if (repoID != null && !currentRepoID.equals(repoID)) {
session = CoreInstance.openCoreSession(repoID);
}
if (templateRepoID != null && !currentRepoID.equals(templateRepoID)) {
templateSession = CoreInstance.openCoreSession(templateRepoID);
}
DocumentModel doc = null;
DocumentModel templateDoc = null;
String filename = null;
if (ACTION_EDIT_DOCUMENT.equals(action)) {
// fetch the document to edit to get its mimetype and document
// type
doc = session.getDocument(new IdRef(docRef));
docType = doc.getType();
Blob blob = null;
if (blobPropertyName != null) {
blob = (Blob) doc.getPropertyValue(blobPropertyName);
if (blob == null) {
throw new NuxeoException(String.format("could not find blob to edit with property '%s'",
blobPropertyName));
}
} else {
blob = (Blob) doc.getProperty(schema, blobField);
if (blob == null) {
throw new NuxeoException(String.format(
"could not find blob to edit with schema '%s' and field '%s'", schema, blobField));
}
}
mimetype = blob.getMimeType();
if (filenamePropertyName != null) {
filename = (String) doc.getPropertyValue(filenamePropertyName);
} else {
filename = (String) doc.getProperty(schema, filenameField);
}
} else if (ACTION_CREATE_DOCUMENT.equals(action)) {
// creating a new document all parameters are read from the
// request parameters
} else if (ACTION_CREATE_DOCUMENT_FROM_TEMPLATE.equals(action)) {
// fetch the template blob to get its mimetype
templateDoc = templateSession.getDocument(new IdRef(templateDocRef));
Blob blob = (Blob) templateDoc.getProperty(templateSchema, templateBlobField);
if (blob == null) {
throw new NuxeoException(String.format(
"could not find template blob with schema '%s' and field '%s'", templateSchema,
templateBlobField));
}
mimetype = blob.getMimeType();
// leave docType from the request query parameter
} else {
throw new NuxeoException(String.format(
"action '%s' is not a valid LiveEdit action: should be one of '%s', '%s' or '%s'", action,
ACTION_CREATE_DOCUMENT, ACTION_CREATE_DOCUMENT_FROM_TEMPLATE, ACTION_EDIT_DOCUMENT));
}
FacesContext context = FacesContext.getCurrentInstance();
HttpServletResponse response = (HttpServletResponse) context.getExternalContext().getResponse();
HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest();
Element root = DocumentFactory.getInstance().createElement(liveEditTag);
root.addNamespace("", XML_LE_NAMESPACE);
// RUX NXP-1959: action id
Element actionInfo = root.addElement(actionSelectorTag);
actionInfo.setText(action);
// Document related informations
Element docInfo = root.addElement(documentTag);
addTextElement(docInfo, docRefTag, docRef);
Element docPathT = docInfo.addElement(docPathTag);
Element docTitleT = docInfo.addElement(docTitleTag);
if (doc != null) {
docPathT.setText(doc.getPathAsString());
docTitleT.setText(doc.getTitle());
}
addTextElement(docInfo, docRepositoryTag, repoID);
addTextElement(docInfo, docSchemaNameTag, schema);
addTextElement(docInfo, docFieldNameTag, blobField);
addTextElement(docInfo, docBlobFieldNameTag, blobField);
Element docFieldPathT = docInfo.addElement(docfieldPathTag);
Element docBlobFieldPathT = docInfo.addElement(docBlobFieldPathTag);
if (blobPropertyName != null) {
// FIXME AT: NXP-2306: send blobPropertyName correctly (?)
docFieldPathT.setText(blobPropertyName);
docBlobFieldPathT.setText(blobPropertyName);
} else {
if (schema != null && blobField != null) {
docFieldPathT.setText(schema + ':' + blobField);
docBlobFieldPathT.setText(schema + ':' + blobField);
}
}
addTextElement(docInfo, docFilenameFieldNameTag, filenameField);
Element docFilenameFieldPathT = docInfo.addElement(docFilenameFieldPathTag);
if (filenamePropertyName != null) {
docFilenameFieldPathT.setText(filenamePropertyName);
} else {
if (schema != null && blobField != null) {
docFilenameFieldPathT.setText(schema + ':' + filenameField);
}
}
addTextElement(docInfo, docfileNameTag, filename);
addTextElement(docInfo, docTypeTag, docType);
addTextElement(docInfo, docMimetypeTag, mimetype);
addTextElement(docInfo, docFileExtensionTag, getFileExtension(mimetype));
Element docFileAuthorizedExtensions = docInfo.addElement(docFileAuthorizedExtensionsTag);
List<String> authorizedExtensions = getFileExtensions(mimetype);
if (authorizedExtensions != null) {
for (String extension : authorizedExtensions) {
addTextElement(docFileAuthorizedExtensions, docFileAuthorizedExtensionTag, extension);
}
}
Element docIsVersionT = docInfo.addElement(docIsVersionTag);
Element docIsLockedT = docInfo.addElement(docIsLockedTag);
if (ACTION_EDIT_DOCUMENT.equals(action)) {
docIsVersionT.setText(Boolean.toString(doc.isVersion()));
docIsLockedT.setText(Boolean.toString(doc.isLocked()));
}
// template information for ACTION_CREATE_DOCUMENT_FROM_TEMPLATE
Element templateDocInfo = root.addElement(templateDocumentTag);
addTextElement(templateDocInfo, docRefTag, templateDocRef);
docPathT = templateDocInfo.addElement(docPathTag);
docTitleT = templateDocInfo.addElement(docTitleTag);
if (templateDoc != null) {
docPathT.setText(templateDoc.getPathAsString());
docTitleT.setText(templateDoc.getTitle());
}
addTextElement(templateDocInfo, docRepositoryTag, templateRepoID);
addTextElement(templateDocInfo, docSchemaNameTag, templateSchema);
addTextElement(templateDocInfo, docFieldNameTag, templateBlobField);
addTextElement(templateDocInfo, docBlobFieldNameTag, templateBlobField);
docFieldPathT = templateDocInfo.addElement(docfieldPathTag);
docBlobFieldPathT = templateDocInfo.addElement(docBlobFieldPathTag);
if (templateSchema != null && templateBlobField != null) {
docFieldPathT.setText(templateSchema + ":" + templateBlobField);
docBlobFieldPathT.setText(templateSchema + ":" + templateBlobField);
}
addTextElement(templateDocInfo, docMimetypeTag, mimetype);
addTextElement(templateDocInfo, docFileExtensionTag, getFileExtension(mimetype));
Element templateFileAuthorizedExtensions = templateDocInfo.addElement(docFileAuthorizedExtensionsTag);
if (authorizedExtensions != null) {
for (String extension : authorizedExtensions) {
addTextElement(templateFileAuthorizedExtensions, docFileAuthorizedExtensionTag, extension);
}
}
// Browser request related informations
Element requestInfo = root.addElement(requestInfoTag);
Cookie[] cookies = request.getCookies();
Element cookiesT = requestInfo.addElement(requestCookiesTag);
for (Cookie cookie : cookies) {
Element cookieT = cookiesT.addElement(requestCookieTag);
cookieT.addAttribute("name", cookie.getName());
cookieT.setText(cookie.getValue());
}
Element headersT = requestInfo.addElement(requestHeadersTag);
Enumeration hEnum = request.getHeaderNames();
while (hEnum.hasMoreElements()) {
String hName = (String) hEnum.nextElement();
if (!hName.equalsIgnoreCase("cookie")) {
Element headerT = headersT.addElement(requestHeaderTag);
headerT.addAttribute("name", hName);
headerT.setText(request.getHeader(hName));
}
}
addTextElement(requestInfo, requestBaseURLTag, BaseURL.getBaseURL(request));
// User related informations
String username = context.getExternalContext().getUserPrincipal().getName();
Element userInfo = root.addElement(userInfoTag);
addTextElement(userInfo, userNameTag, username);
addTextElement(userInfo, userPasswordTag, "");
addTextElement(userInfo, userTokenTag, "");
addTextElement(userInfo, userLocaleTag, context.getViewRoot().getLocale().toString());
// Rux NXP-1882: the wsdl locations
String baseUrl = BaseURL.getBaseURL(request);
Element wsdlLocations = root.addElement(wsdlLocationsTag);
Element wsdlAccessWST = wsdlLocations.addElement(wsdlAccessWebServiceTag);
wsdlAccessWST.setText(baseUrl + "webservices/nuxeoAccess?wsdl");
Element wsdlEEWST = wsdlLocations.addElement(wsdlLEWebServiceTag);
wsdlEEWST.setText(baseUrl + "webservices/nuxeoLEWS?wsdl");
// Server related informations
Element serverInfo = root.addElement(serverInfoTag);
Element serverVersionT = serverInfo.addElement(serverVersionTag);
serverVersionT.setText("5.1"); // TODO: use a buildtime generated
// version tag instead
// Client related informations
Element editId = root.addElement(editIdTag);
editId.setText(getEditId(doc, session, username));
// serialize bootstrap XML document in the response
Document xmlDoc = DocumentFactory.getInstance().createDocument();
xmlDoc.setRootElement(root);
response.setContentType("text/xml; charset=UTF-8");
// use a formatter to make it easier to debug live edit client
// implementations
OutputFormat format = OutputFormat.createPrettyPrint();
format.setEncoding("UTF-8");
XMLWriter writer = new XMLWriter(response.getOutputStream(), format);
writer.write(xmlDoc);
response.flushBuffer();
context.responseComplete();
} finally {
if (session != null && session != documentManager) {
session.close();
}
if (templateSession != null && templateSession != documentManager) {
templateSession.close();
}
}
}
protected String getFileExtension(String mimetype) {
if (mimetype == null) {
return null;
}
MimetypeRegistry mimetypeRegistry = Framework.getService(MimetypeRegistry.class);
List<String> extensions = mimetypeRegistry.getExtensionsFromMimetypeName(mimetype);
if (extensions != null && !extensions.isEmpty()) {
return extensions.get(0);
} else {
return null;
}
}
protected List<String> getFileExtensions(String mimetype) {
if (mimetype == null) {
return null;
}
MimetypeRegistry mimetypeRegistry = Framework.getService(MimetypeRegistry.class);
List<String> extensions = mimetypeRegistry.getExtensionsFromMimetypeName(mimetype);
return extensions;
}
protected static Element addTextElement(Element parent, QName newElementName, String value) {
Element element = parent.addElement(newElementName);
if (value != null) {
element.setText(value);
}
return element;
}
// TODO: please explain what is the use of the "editId" tag here
protected static String getEditId(DocumentModel doc, CoreSession session, String userName) {
StringBuilder sb = new StringBuilder();
if (doc != null) {
sb.append(doc.getId());
} else {
sb.append("NewDocument");
}
sb.append('-');
sb.append(session.getRepositoryName());
sb.append('-');
sb.append(userName);
Calendar modified = null;
if (doc != null) {
try {
modified = (Calendar) doc.getProperty(DUBLINCORE_SCHEMA, MODIFIED_FIELD);
} catch (PropertyException e) {
modified = null;
}
}
if (modified == null) {
modified = Calendar.getInstance();
}
sb.append('-');
sb.append(modified.getTimeInMillis());
return sb.toString();
}
//
// Methods to check whether or not to display live edit links
//
/**
* @deprecated use {@link #isLiveEditable(DocumentModel doc, String blobXpath)}
*/
@Deprecated
public boolean isLiveEditable(Blob blob) {
if (blob == null) {
return false;
}
String mimetype = blob.getMimeType();
return isMimeTypeLiveEditable(mimetype);
}
/**
* @param document the document to edit.
* @param blobXPath XPath to the blob property
* @return true if the document is immutable and the blob's mime type is supported, false otherwise.
* @since 5.4
*/
public boolean isLiveEditable(DocumentModel document, Blob blob) {
if (document.isImmutable()) {
return false;
}
// NXP-14476: Testing lifecycle state is part of the "mutable_document" filter
if (document.getCurrentLifeCycleState().equals(LifeCycleConstants.DELETED_STATE)) {
return false;
}
if (blob == null) {
return false;
}
String mimetype = blob.getMimeType();
return isMimeTypeLiveEditable(mimetype);
}
public boolean isMimeTypeLiveEditable(Blob blob) {
if (blob == null) {
return false;
}
String mimetype = blob.getMimeType();
return isMimeTypeLiveEditable(mimetype);
}
public boolean isMimeTypeLiveEditable(String mimetype) {
Boolean isEditable = cachedEditableStates.get(mimetype);
if (isEditable == null) {
if (liveEditClientConfig.getLiveEditConfigurationPolicy().equals(LiveEditClientConfig.LE_CONFIG_CLIENTSIDE)) {
// only trust client config
isEditable = liveEditClientConfig.isMimeTypeLiveEditable(mimetype);
cachedEditableStates.put(mimetype, isEditable);
return isEditable;
}
MimetypeEntry mimetypeEntry = getMimetypeRegistry().getMimetypeEntryByMimeType(mimetype);
if (mimetypeEntry == null) {
isEditable = Boolean.FALSE;
} else {
isEditable = mimetypeEntry.isOnlineEditable();
}
if (liveEditClientConfig.getLiveEditConfigurationPolicy().equals(LiveEditClientConfig.LE_CONFIG_BOTHSIDES)) {
boolean isEditableOnClient = liveEditClientConfig.isMimeTypeLiveEditable(mimetype);
isEditable = isEditable && isEditableOnClient;
}
cachedEditableStates.put(mimetype, isEditable);
}
return isEditable;
}
@Factory(value = "msword_liveeditable", scope = ScopeType.SESSION)
public boolean isMSWordLiveEdititable() {
return isMimeTypeLiveEditable("application/msword");
}
@Factory(value = "msexcel_liveeditable", scope = ScopeType.SESSION)
public boolean isMSExcelLiveEdititable() {
return isMimeTypeLiveEditable("application/vnd.ms-excel");
}
@Factory(value = "mspowerpoint_liveeditable", scope = ScopeType.SESSION)
public boolean isMSPowerpointLiveEdititable() {
return isMimeTypeLiveEditable("application/vnd.ms-powerpoint");
}
@Factory(value = "ootext_liveeditable", scope = ScopeType.SESSION)
public boolean isOOTextLiveEdititable() {
return isMimeTypeLiveEditable("application/vnd.oasis.opendocument.text");
}
@Factory(value = "oocalc_liveeditable", scope = ScopeType.SESSION)
public boolean isOOCalcLiveEdititable() {
return isMimeTypeLiveEditable("application/vnd.oasis.opendocument.spreadsheet");
}
@Factory(value = "oopresentation_liveeditable", scope = ScopeType.SESSION)
public boolean isOOPresentationLiveEdititable() {
return isMimeTypeLiveEditable("application/vnd.oasis.opendocument.presentation");
}
public boolean isCurrentDocumentLiveEditable() {
return isDocumentLiveEditable(navigationContext.getCurrentDocument(), DEFAULT_SCHEMA, DEFAULT_BLOB_FIELD);
}
public boolean isCurrentDocumentLiveEditable(String schemaName, String fieldName) {
return isDocumentLiveEditable(navigationContext.getCurrentDocument(), schemaName, fieldName);
}
public boolean isCurrentDocumentLiveEditable(String propertyName) {
return isDocumentLiveEditable(navigationContext.getCurrentDocument(), propertyName);
}
public boolean isDocumentLiveEditable(DocumentModel documentModel, String schemaName, String fieldName)
{
return isDocumentLiveEditable(documentModel, schemaName + ":" + fieldName);
}
public boolean isDocumentLiveEditable(DocumentModel documentModel, String propertyName) {
if (documentModel == null) {
log.warn("cannot check live editable state of null DocumentModel");
return false;
}
// NXP-14476: Testing lifecycle state is part of the "mutable_document" filter
if (LifeCycleConstants.DELETED_STATE.equals(documentModel.getCurrentLifeCycleState())) {
return false;
}
// check Client browser config
if (!liveEditClientConfig.isLiveEditInstalled()) {
return false;
}
String cacheKey = documentModel.getRef() + "__" + propertyName;
Boolean cachedEditableBlob = cachedEditableBlobs.get(cacheKey);
if (cachedEditableBlob == null) {
if (documentModel.hasFacet(FacetNames.IMMUTABLE)) {
return cacheBlobToFalse(cacheKey);
}
if (!documentManager.hasPermission(documentModel.getRef(), SecurityConstants.WRITE_PROPERTIES)) {
// the lock state is check as a extension to the
// SecurityPolicyManager
return cacheBlobToFalse(cacheKey);
}
Blob blob;
try {
blob = documentModel.getProperty(propertyName).getValue(Blob.class);
} catch (PropertyException e) {
// this document cannot host a live editable blob is the
// requested property, ignore
return cacheBlobToFalse(cacheKey);
}
cachedEditableBlob = isLiveEditable(blob);
cachedEditableBlobs.put(cacheKey, cachedEditableBlob);
}
return cachedEditableBlob;
}
protected boolean cacheBlobToFalse(String cacheKey) {
cachedEditableBlobs.put(cacheKey, Boolean.FALSE);
return false;
}
protected MimetypeRegistry getMimetypeRegistry() {
if (mimetypeRegistry == null) {
mimetypeRegistry = Framework.getService(MimetypeRegistry.class);
}
return mimetypeRegistry;
}
}