/** * OpenKM, Open Document Management System (http://www.openkm.com) * Copyright (c) 2006-2011 Paco Avila & Josep Llort * * No bytes were intentionally harmed during the development of this application. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package com.openkm.webdav; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import java.util.regex.Pattern; import javax.jcr.AccessDeniedException; import javax.jcr.Item; import javax.jcr.ItemNotFoundException; import javax.jcr.NamespaceException; import javax.jcr.NamespaceRegistry; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.Property; import javax.jcr.PropertyIterator; import javax.jcr.PropertyType; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.Value; import javax.jcr.nodetype.PropertyDefinition; import org.apache.jackrabbit.JcrConstants; import org.apache.jackrabbit.server.io.ExportContext; import org.apache.jackrabbit.server.io.IOHandler; import org.apache.jackrabbit.server.io.IOManager; import org.apache.jackrabbit.server.io.IOUtil; import org.apache.jackrabbit.server.io.ImportContext; import org.apache.jackrabbit.server.io.PropertyExportContext; import org.apache.jackrabbit.server.io.PropertyHandler; import org.apache.jackrabbit.server.io.PropertyImportContext; import org.apache.jackrabbit.util.ISO9075; import org.apache.jackrabbit.util.Text; import org.apache.jackrabbit.webdav.DavResource; import org.apache.jackrabbit.webdav.property.DavProperty; import org.apache.jackrabbit.webdav.property.DavPropertyName; import org.apache.jackrabbit.webdav.xml.Namespace; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.openkm.bean.Document; import com.openkm.bean.Folder; import com.openkm.bean.Permission; import com.openkm.cache.UserItemsManager; import com.openkm.core.Config; import com.openkm.core.DatabaseException; import com.openkm.dao.MimeTypeDAO; import com.openkm.extractor.RegisteredExtractors; import com.openkm.jcr.JCRUtils; public class DefaultHandler implements IOHandler, PropertyHandler { private static Logger log = LoggerFactory.getLogger(DefaultHandler.class); //private String collectionNodetype = JcrConstants.NT_FOLDER; private String collectionNodetype = Folder.TYPE; //private String defaultNodetype = JcrConstants.NT_FILE; private String defaultNodetype = Document.TYPE; /* IMPORTANT NOTE: for webDAV compliancy the default nodetype of the content node has been changed from nt:resource to nt:unstructured. */ //private String contentNodetype = JcrConstants.NT_UNSTRUCTURED; private String contentNodetype = Document.CONTENT_TYPE; private IOManager ioManager; /** * Creates a new <code>DefaultHandler</code> with default nodetype definitions * and without setting the IOManager. * * @see OKMHandler#setIOManager(IOManager) */ public DefaultHandler() { log.debug("DefaultHandler()"); } /** * Creates a new <code>DefaultHandler</code> with default nodetype definitions:<br> * <ul> * <li>Nodetype for Collection: {@link JcrConstants#NT_FOLDER nt:folder}</li> * <li>Nodetype for Non-Collection: {@link JcrConstants#NT_FILE nt:file}</li> * <li>Nodetype for Non-Collection content: {@link JcrConstants#NT_RESOURCE nt:resource}</li> * </ul> * * @param ioManager */ public DefaultHandler(IOManager ioManager) { log.debug("DefaultHandler({})", ioManager); this.ioManager = ioManager; } /** * Creates a new <code>DefaultHandler</code>. Please note that the specified * nodetypes must match the definitions of the defaults. * * @param ioManager * @param collectionNodetype * @param defaultNodetype * @param contentNodetype */ public DefaultHandler(IOManager ioManager, String collectionNodetype, String defaultNodetype, String contentNodetype) { log.debug("DefaultHandler({}, {}, {}, {})", new Object[] { ioManager, collectionNodetype, defaultNodetype, contentNodetype }); this.ioManager = ioManager; this.collectionNodetype = collectionNodetype; this.defaultNodetype = defaultNodetype; this.contentNodetype = contentNodetype; } /** * @see OKMHandler#getIOManager() */ public IOManager getIOManager() { return ioManager; } /** * @see OKMHandler#setIOManager(IOManager) */ public void setIOManager(IOManager ioManager) { this.ioManager = ioManager; } /** * @see OKMHandler#getName() */ public String getName() { return getClass().getName(); } /** * @see OKMHandler#canImport(ImportContext, boolean) */ public boolean canImport(ImportContext context, boolean isCollection) { log.debug("canImport({}, {})", context, isCollection); if (context == null || context.isCompleted()) { return false; } Item contextItem = context.getImportRoot(); if (!isCollection) { // Check file restrictions (Don't check folders) String mimeType = Config.mimeTypes.getContentType(context.getSystemId().toLowerCase()); log.debug("File: {}", context.getSystemId()); log.debug("MimeType: {}", mimeType); log.debug("Size: {}", context.getContentLength()); // Restrict for MIME try { if (Config.RESTRICT_FILE_MIME && MimeTypeDAO.findByName(mimeType) == null) { return false; } } catch (DatabaseException e) { log.error(e.getMessage(), e); return false; } // Restrict for extension StringTokenizer st = new StringTokenizer(Config.RESTRICT_FILE_EXTENSION, ","); while (st.hasMoreTokens()) { String wc = st.nextToken(); String re = wildcard2regexp(wc); if (Pattern.matches(re, context.getSystemId())) { log.debug("Filename BAD -> {} ({})", re, wc); return false; } else { log.debug("Filename GOOD -> {} ({})", re, wc); } } // Restrict for size if (context.getContentLength() > Config.MAX_FILE_SIZE) { return false; } } return contextItem != null && contextItem.isNode() && context.getSystemId() != null; } /** * @param wildcard * @return */ private String wildcard2regexp(String wildcard) { StringBuffer sb = new StringBuffer("^"); for (int i = 0; i < wildcard.length(); i++) { char c = wildcard.charAt(i); switch (c) { case '.': sb.append("\\."); break; case '*': sb.append(".*"); break; case '?': sb.append("."); break; default: sb.append(c); break; } } return sb.toString(); } /** * @see OKMHandler#canImport(ImportContext, DavResource) */ public boolean canImport(ImportContext context, DavResource resource) { //log.debug("canImport("+context+", "+resource+")"); if (resource == null) { return false; } return canImport(context, resource.isCollection()); } /** * @see OKMHandler#importContent(ImportContext, boolean) */ public boolean importContent(ImportContext context, boolean isCollection) throws IOException { log.debug("importContent({}, {})", context, isCollection); if (!canImport(context, isCollection)) { throw new IOException(getName() + ": Cannot import " + context.getSystemId()); } boolean success = false; try { Node contentNode = getContentNode(context, isCollection); log.info("contentNode: {}", contentNode.getPath()); if (contentNode.isNodeType("mix:versionable")) { log.debug("CHECKOUT"); contentNode.checkout(); } success = importProperties(context, isCollection, contentNode); if (success) { success = importData(context, isCollection, contentNode); } if (contentNode.isNodeType(Document.CONTENT_TYPE)) { contentNode.getParent().getParent().save(); // Check document filters //DocumentUtils.checkFilters(session, mainNode, mimeType); } if (contentNode.isNodeType("mix:versionable")) { log.debug("CHECKIN"); // Esta línea vale millones!! Resuelve la incidencia del isCkechedOut. // Por lo visto un nuevo nodo se añade con el isCheckedOut a true :/ javax.jcr.version.Version ver = contentNode.checkin(); if (Config.USER_ITEM_CACHE) { // Update user items String user = contentNode.getSession().getUserID(); long size = contentNode.getProperty(Document.SIZE).getLong(); UserItemsManager.incSize(user, size); if (ver.getName().equals("1.0")) { UserItemsManager.incDocuments(user, 1); } } } // Remove pdf & preview from cache Node documentNode = contentNode.getParent(); log.info("Delete: {}", Config.CACHE_DXF + File.separator + documentNode.getUUID() + ".dxf"); new File(Config.CACHE_DXF + File.separator + documentNode.getUUID() + ".dxf").delete(); log.info("Delete: {}", Config.CACHE_PDF + File.separator + documentNode.getUUID() + ".pdf"); new File(Config.CACHE_PDF + File.separator + documentNode.getUUID() + ".pdf").delete(); log.info("Delete: {}", Config.CACHE_SWF + File.separator + documentNode.getUUID() + ".swf"); new File(Config.CACHE_SWF + File.separator + documentNode.getUUID() + ".swf").delete(); } catch (RepositoryException e) { success = false; throw new IOException(e.getMessage()); } finally { // revert any changes made in case the import failed. if (!success) { try { context.getImportRoot().refresh(false); } catch (RepositoryException e) { throw new IOException(e.getMessage()); } } } return success; } /** * @see OKMHandler#importContent(ImportContext, DavResource) */ public boolean importContent(ImportContext context, DavResource resource) throws IOException { log.debug("importContent({}, {})", context, resource); if (!canImport(context, resource)) { throw new IOException(getName() + ": Cannot import " + context.getSystemId()); } return importContent(context, resource.isCollection()); } /** * Imports the data present on the import context to the specified content * node. * * @param context * @param isCollection * @param contentNode * @return * @throws IOException */ protected boolean importData(ImportContext context, boolean isCollection, Node contentNode) throws IOException, RepositoryException { log.debug("importData({}, {}, {})", new Object[] {context, isCollection, contentNode }); InputStream is = context.getInputStream(); if (is != null) { // NOTE: with the default folder-nodetype (nt:folder) no inputstream // is allowed. setting the property would therefore fail. if (isCollection) { return false; } try { contentNode.setProperty(JcrConstants.JCR_DATA, is); if (Config.EXPERIMENTAL_TEXT_EXTRACTION) { String mimeType = contentNode.getProperty(JcrConstants.JCR_MIMETYPE).getString(); RegisteredExtractors.index(contentNode.getParent(), contentNode, mimeType); } } finally { is.close(); } } // success if no data to import. return true; } /** * Imports the properties present on the specified context to the content * node. */ protected boolean importProperties(ImportContext context, boolean isCollection, Node contentNode) { log.debug("importProperties({}, {}, {})", new Object[] { context, isCollection, contentNode }); String mimeType = null; try { // set mimeType property upon resource creation but don't modify // it on a subsequent PUT. In contrast to a PROPPATCH request, which // is handled by #importProperties(PropertyContext, boolean)} if (!contentNode.hasProperty(JcrConstants.JCR_MIMETYPE)) { //contentNode.setProperty(JcrConstants.JCR_MIMETYPE, context.getMimeType()); mimeType = Config.mimeTypes.getContentType(context.getSystemId().toLowerCase()); contentNode.setProperty(JcrConstants.JCR_MIMETYPE, mimeType); } } catch (RepositoryException e) { // ignore: property may not be present on the node } try { // set encoding property upon resource creation but don't modify // it on a subsequent PUT. In contrast to a PROPPATCH request, which // is handled by #importProperties(PropertyContext, boolean)} if (!contentNode.hasProperty(JcrConstants.JCR_ENCODING)) { contentNode.setProperty(JcrConstants.JCR_ENCODING, context.getEncoding()); } } catch (RepositoryException e) { // ignore: property may not be present on the node } setLastModified(contentNode, context.getModificationTime()); // OpenKM try { Session session = contentNode.getSession(); Node parentNode = null; Node mainNode = null; if (contentNode.isNodeType(Folder.TYPE)) { log.debug("Folder node type"); Node folderNode = contentNode; parentNode = folderNode.getParent(); // Basic folder properties folderNode.setProperty(Folder.AUTHOR, session.getUserID()); folderNode.setProperty(Folder.NAME, folderNode.getName()); // Set main node mainNode = folderNode; } else if (contentNode.isNodeType(Document.CONTENT_TYPE)) { log.debug("Document node type"); Node documentNode = contentNode.getParent(); parentNode = documentNode.getParent(); // Basic document properties documentNode.setProperty(com.openkm.bean.Property.KEYWORDS, new String[]{}); documentNode.setProperty(com.openkm.bean.Property.CATEGORIES, new String[]{}, PropertyType.REFERENCE); documentNode.setProperty(Document.AUTHOR, session.getUserID()); documentNode.setProperty(Document.NAME, documentNode.getName()); // Basic content properties contentNode.setProperty(Document.SIZE, context.getContentLength()); contentNode.setProperty(Document.AUTHOR, session.getUserID()); contentNode.setProperty(Document.VERSION_COMMENT, "Edited using WebDAV"); // Set main node mainNode = documentNode; } // Get parent node auth info Value[] usersReadParent = parentNode.getProperty(Permission.USERS_READ).getValues(); String[] usersRead = JCRUtils.usrValue2String(usersReadParent, session.getUserID()); Value[] usersWriteParent = parentNode.getProperty(Permission.USERS_WRITE).getValues(); String[] usersWrite = JCRUtils.usrValue2String(usersWriteParent, session.getUserID()); Value[] usersDeleteParent = parentNode.getProperty(Permission.USERS_DELETE).getValues(); String[] usersDelete = JCRUtils.usrValue2String(usersDeleteParent, session.getUserID()); Value[] usersSecurityParent = parentNode.getProperty(Permission.USERS_SECURITY).getValues(); String[] usersSecurity = JCRUtils.usrValue2String(usersSecurityParent, session.getUserID()); Value[] rolesReadParent = parentNode.getProperty(Permission.ROLES_READ).getValues(); String[] rolesRead = JCRUtils.rolValue2String(rolesReadParent); Value[] rolesWriteParent = parentNode.getProperty(Permission.ROLES_WRITE).getValues(); String[] rolesWrite = JCRUtils.rolValue2String(rolesWriteParent); Value[] rolesDeleteParent = parentNode.getProperty(Permission.ROLES_DELETE).getValues(); String[] rolesDelete = JCRUtils.rolValue2String(rolesDeleteParent); Value[] rolesSecurityParent = parentNode.getProperty(Permission.ROLES_SECURITY).getValues(); String[] rolesSecurity = JCRUtils.rolValue2String(rolesSecurityParent); // Set auth info mainNode.setProperty(Permission.USERS_READ, usersRead); mainNode.setProperty(Permission.USERS_WRITE, usersWrite); mainNode.setProperty(Permission.USERS_DELETE, usersDelete); mainNode.setProperty(Permission.USERS_SECURITY, usersSecurity); mainNode.setProperty(Permission.ROLES_READ, rolesRead); mainNode.setProperty(Permission.ROLES_WRITE, rolesWrite); mainNode.setProperty(Permission.ROLES_DELETE, rolesDelete); mainNode.setProperty(Permission.ROLES_SECURITY, rolesSecurity); } catch (ItemNotFoundException e) { e.printStackTrace(); } catch (AccessDeniedException e) { e.printStackTrace(); } catch (RepositoryException e) { e.printStackTrace(); } return true; } /** * Retrieves/creates the node that will be used to import properties and * data. In case of a non-collection this includes and additional content node * to be created beside the 'file' node.<br> * Please note: If the jcr:content node already exists and contains child * nodes, those will be removed in order to make sure, that the import * really replaces the existing content of the file-node. */ protected Node getContentNode(ImportContext context, boolean isCollection) throws RepositoryException { log.debug("getContentNode({}, {})", context, isCollection); Node parentNode = (Node)context.getImportRoot(); String name = context.getSystemId(); if (parentNode.hasNode(name)) { parentNode = parentNode.getNode(name); } else { String ntName = (isCollection) ? getCollectionNodeType() : getNodeType(); parentNode = parentNode.addNode(name, ntName); } Node contentNode = null; if (isCollection) { contentNode = parentNode; } else { if (parentNode.hasNode(Document.CONTENT)) { contentNode = parentNode.getNode(Document.CONTENT); // check if nodetype is compatible (might be update of an existing file) if (contentNode.isNodeType(getContentNodeType())) { // remove all entries in the jcr:content since replacing content // includes properties (DefaultHandler) and nodes (e.g. ZipHandler) if (contentNode.hasNodes()) { NodeIterator it = contentNode.getNodes(); while (it.hasNext()) { it.nextNode().remove(); } } } else { contentNode.remove(); contentNode = null; } } if (contentNode == null) { contentNode = parentNode.addNode(Document.CONTENT, getContentNodeType()); } } return contentNode; } /** * Returns true if the export root is a node and if it contains a child node * with name {@link JcrConstants#JCR_CONTENT jcr:content} in case this * export is not intended for a collection. * * @return true if the export root is a node. If the specified boolean paramter * is false (not a collection export) the given export root must contain a * child node with name {@link JcrConstants#JCR_CONTENT jcr:content}. * * @see OKMHandler#canExport(ExportContext, boolean) */ @Override public boolean canExport(ExportContext context, boolean isCollection) { log.debug("canExport(ExportContext:{}, {})", context, isCollection); if (context == null || context.isCompleted()) { return false; } Item exportRoot = context.getExportRoot(); boolean success = exportRoot != null && exportRoot.isNode(); if (success && !isCollection) { try { Node n = ((Node)exportRoot); log.debug("Path: {}", n.getPath()); //for (NodeIterator nit = n.getNodes(); nit.hasNext(); ) { //log.debug("### "+nit.nextNode().getPath()); //} success = n.hasNode(Document.CONTENT); } catch (RepositoryException e) { // should never occur. success = false; } } return success; } /** * @see OKMHandler#canExport(ExportContext, DavResource) */ @Override public boolean canExport(ExportContext context, DavResource resource) { log.debug("canExport(ExportContext:{}, DavResource:{})", context, resource); if (resource == null) { return false; } return canExport(context, resource.isCollection()); } /** * Retrieves the content node that will be used for exporting properties and * data and calls the corresponding methods. * * @param context * @param isCollection * @see #exportProperties(ExportContext, boolean, Node) * @see #exportData(ExportContext, boolean, Node) */ @Override public boolean exportContent(ExportContext context, boolean isCollection) throws IOException { log.debug("exportContent({}, {})", context, isCollection); if (!canExport(context, isCollection)) { throw new IOException(getName() + ": Cannot export " + context.getExportRoot()); } try { Node contentNode = getContentNode(context, isCollection); exportProperties(context, isCollection, contentNode); if (context.hasStream()) { exportData(context, isCollection, contentNode); } // else: missing stream. ignore. return true; } catch (RepositoryException e) { // should never occur, since the proper structure of the content // node must be asserted in the 'canExport' call. throw new IOException(e.getMessage()); } } /** * Same as (@link IOHandler#exportContent(ExportContext, boolean)} where * the boolean values is defined by {@link DavResource#isCollection()}. * * @see OKMHandler#exportContent(ExportContext, DavResource) */ @Override public boolean exportContent(ExportContext context, DavResource resource) throws IOException { log.debug("exportContent({}, {})", context, resource); if (!canExport(context, resource)) { throw new IOException(getName() + ": Cannot export " + context.getExportRoot()); } return exportContent(context, resource.isCollection()); } /** * Checks if the given content node contains a jcr:data property * and spools its value to the output stream fo the export context.<br> * Please note, that subclasses that define a different structure of the * content node should create their own * {@link #exportData(ExportContext, boolean, Node) exportData} method. */ protected void exportData(ExportContext context, boolean isCollection, Node contentNode) throws IOException, RepositoryException { log.debug("exportData({}, {}, {})", new Object[] { context, isCollection, contentNode }); if (contentNode.hasProperty(JcrConstants.JCR_DATA)) { Property p = contentNode.getProperty(JcrConstants.JCR_DATA); IOUtil.spool(p.getStream(), context.getOutputStream()); } // else: stream undefined -> contentlength was not set } /** * Retrieves mimetype, encoding and modification time from the content node. * The content length is determined by the length of the jcr:data property * if it is present. The creation time however is retrieved from the parent * node (in case of isCollection == false only). */ protected void exportProperties(ExportContext context, boolean isCollection, Node contentNode) throws IOException { log.debug("exportProperties({}, {}, {})", new Object[] { context, isCollection, contentNode }); try { // only non-collections: 'jcr:created' is present on the parent 'fileNode' only if (!isCollection && contentNode.getDepth() > 0 && contentNode.getParent().hasProperty(JcrConstants.JCR_CREATED)) { long cTime = contentNode.getParent().getProperty(JcrConstants.JCR_CREATED).getValue().getLong(); context.setCreationTime(cTime); } long length = IOUtil.UNDEFINED_LENGTH; if (contentNode.hasProperty(JcrConstants.JCR_DATA)) { Property p = contentNode.getProperty(JcrConstants.JCR_DATA); length = p.getLength(); context.setContentLength(length); } String mimeType = null; String encoding = null; if (contentNode.hasProperty(JcrConstants.JCR_MIMETYPE)) { mimeType = contentNode.getProperty(JcrConstants.JCR_MIMETYPE).getString(); } if (contentNode.hasProperty(JcrConstants.JCR_ENCODING)) { encoding = contentNode.getProperty(JcrConstants.JCR_ENCODING).getString(); // ignore "" encodings (although this is avoided during import) if ("".equals(encoding)) { encoding = null; } } context.setContentType(mimeType, encoding); long modTime = IOUtil.UNDEFINED_TIME; if (contentNode.hasProperty(JcrConstants.JCR_LASTMODIFIED)) { modTime = contentNode.getProperty(JcrConstants.JCR_LASTMODIFIED).getLong(); context.setModificationTime(modTime); } else { context.setModificationTime(System.currentTimeMillis()); } if (length > IOUtil.UNDEFINED_LENGTH && modTime > IOUtil.UNDEFINED_TIME) { String etag = "\"" + length + "-" + modTime + "\""; context.setETag(etag); } } catch (RepositoryException e) { // should never occur log.error("Unexpected error {} while exporting properties: {}", e.getClass().getName(), e.getMessage()); throw new IOException(e.getMessage()); } } /** * Retrieves the content node that contains the data to be exported. In case * isCollection is true, this corresponds to the export root. Otherwise there * must be a child node with name {@link JcrConstants#JCR_CONTENT jcr:content}. * * @param context * @param isCollection * @return content node used for the export * @throws RepositoryException */ protected Node getContentNode(ExportContext context, boolean isCollection) throws RepositoryException { log.debug("getContentNode({}, {})", context, isCollection); Node contentNode = (Node)context.getExportRoot(); // 'file' nodes must have an jcr:content child node (see canExport) if (!isCollection) { contentNode = contentNode.getNode(Document.CONTENT); } return contentNode; } /** * Name of the nodetype to be used to create a new collection node (folder) * * @return nodetype name */ protected String getCollectionNodeType() { //log.debug("getCollectionNodeType: {}", collectionNodetype); return collectionNodetype; } /** * Name of the nodetype to be used to create a new non-collection node (file) * * @return nodetype name */ protected String getNodeType() { //log.debug("getNodeType: {}", defaultNodetype); return defaultNodetype; } /** * Name of the nodetype to be used to create the content node below * a new non-collection node, whose name is always {@link JcrConstants#JCR_CONTENT * jcr:content}. * * @return nodetype name */ protected String getContentNodeType() { //log.debug("getContentNodeType: {}", contentNodetype); return contentNodetype; } //----------------------------------------------------< PropertyHandler >--- @Override public boolean canExport(PropertyExportContext context, boolean isCollection) { log.debug("canExport(PropertyExportContext:{}, {})", context, isCollection); return canExport((ExportContext) context, isCollection); } @Override public boolean exportProperties(PropertyExportContext exportContext, boolean isCollection) throws RepositoryException { log.debug("exportProperties({}, {}", exportContext, isCollection); if (!canExport(exportContext, isCollection)) { throw new RepositoryException("PropertyHandler " + getName() + " failed to export properties."); } Node cn = getContentNode(exportContext, isCollection); try { // export the properties common with normal IO handling exportProperties(exportContext, isCollection, cn); // export all other properties as well PropertyIterator it = cn.getProperties(); while (it.hasNext()) { Property p = it.nextProperty(); String name = p.getName(); PropertyDefinition def = p.getDefinition(); if (def.isMultiple() || isDefinedByFilteredNodeType(def)) { log.debug("Skip property '{}': not added to webdav property set.", name); continue; } if (JcrConstants.JCR_DATA.equals(name) || JcrConstants.JCR_MIMETYPE.equals(name) || JcrConstants.JCR_ENCODING.equals(name) || JcrConstants.JCR_LASTMODIFIED.equals(name)) { continue; } DavPropertyName davName = getDavName(name, p.getSession()); exportContext.setProperty(davName, p.getValue().getString()); } return true; } catch (IOException e) { // should not occur (log output see 'exportProperties') return false; } } @Override public boolean canImport(PropertyImportContext context, boolean isCollection) { log.debug("canImport({}, {})", context, isCollection); if (context == null || context.isCompleted()) { return false; } Item contextItem = context.getImportRoot(); try { return contextItem != null && contextItem.isNode() && (isCollection || ((Node)contextItem).hasNode(JcrConstants.JCR_CONTENT)); } catch (RepositoryException e) { log.error("Unexpected error: {}", e.getMessage()); return false; } } @Override @SuppressWarnings({ "rawtypes", "unchecked" }) public Map importProperties(PropertyImportContext importContext, boolean isCollection) throws RepositoryException { log.debug("importProperties({}, {})", importContext, isCollection); if (!canImport(importContext, isCollection)) { throw new RepositoryException("PropertyHandler " + getName() + " failed import properties"); } // loop over List and remember all properties and propertyNames // that failed to be imported (set or remove). Map failures = new HashMap(); List changeList = importContext.getChangeList(); // for collections the import-root is the target node where properties // are altered. in contrast 'non-collections' are with the handler // represented by 'file' nodes, that must have a jcr:content child // node, which holds all properties except jcr:created. // -> see canImport for the corresponding assertions Node cn = (Node) importContext.getImportRoot(); if (!isCollection && cn.hasNode(JcrConstants.JCR_CONTENT)) { cn = cn.getNode(JcrConstants.JCR_CONTENT); } if (changeList != null) { Iterator it = changeList.iterator(); while (it.hasNext()) { Object propEntry = it.next(); try { if (propEntry instanceof DavPropertyName) { // remove DavPropertyName propName = (DavPropertyName) propEntry; removeJcrProperty(propName, cn); } else if (propEntry instanceof DavProperty) { // add or modify property DavProperty prop = (DavProperty)propEntry; setJcrProperty(prop, cn); } else { // ignore any other entry in the change list log.error("unknown object in change list: " + propEntry.getClass().getName()); } } catch (RepositoryException e) { failures.put(propEntry, e); } } } if (failures.isEmpty()) { setLastModified(cn, IOUtil.UNDEFINED_LENGTH); } return failures; } //------------------------------------------------------------< private >--- /** * Builds a webdav property name from the given jcrName. In case the jcrName * contains a namespace prefix that would conflict with any of the predefined * webdav namespaces a new prefix is assigned.<br> * Please note, that the local part of the jcrName is checked for XML * compatibility by calling {@link ISO9075#encode(String)} * * @param jcrName * @param session * @return a <code>DavPropertyName</code> for the given jcr name. */ private DavPropertyName getDavName(String jcrName, Session session) throws RepositoryException { //log.debug("getDavName({}, {})", jcrName, session); // make sure the local name is xml compliant String localName = ISO9075.encode(Text.getLocalName(jcrName)); String prefix = Text.getNamespacePrefix(jcrName); String uri = session.getNamespaceURI(prefix); Namespace namespace = Namespace.getNamespace(prefix, uri); DavPropertyName name = DavPropertyName.create(localName, namespace); //log.debug("getDavName: {}", name); return name; } /** * Build jcr property name from dav property name. If the property name * defines a namespace uri, that has not been registered yet, an attempt * is made to register the uri with the prefix defined. Note, that no * extra effort is made to generated a unique prefix. * * @param propName * @return jcr name * @throws RepositoryException */ private String getJcrName(DavPropertyName propName, Session session) throws RepositoryException { // remove any encoding necessary for xml compliance String pName = ISO9075.decode(propName.getName()); Namespace propNamespace = propName.getNamespace(); if (!Namespace.EMPTY_NAMESPACE.equals(propNamespace)) { String prefix; String emptyPrefix = Namespace.EMPTY_NAMESPACE.getPrefix(); try { // lookup 'prefix' in the session-ns-mappings / namespace-registry prefix = session.getNamespacePrefix(propNamespace.getURI()); } catch (NamespaceException e) { // namespace uri has not been registered yet NamespaceRegistry nsReg = session.getWorkspace().getNamespaceRegistry(); prefix = propNamespace.getPrefix(); // avoid trouble with default namespace if (emptyPrefix.equals(prefix)) { prefix = "_pre" + nsReg.getPrefixes().length + 1; } // NOTE: will fail if prefix is already in use in the namespace registry nsReg.registerNamespace(prefix, propNamespace.getURI()); } if (prefix != null && !emptyPrefix.equals(prefix)) { pName = prefix + ":" + pName; } } return pName; } /** * @param property * @throws RepositoryException */ private void setJcrProperty(DavProperty property, Node contentNode) throws RepositoryException { // Retrieve the property value. Note, that a 'null' value is replaced // by empty string, since setting a jcr property value to 'null' // would be equivalent to its removal. String value = ""; if (property.getValue() != null) { value = property.getValue().toString(); } DavPropertyName davName = property.getName(); if (DavPropertyName.GETCONTENTTYPE.equals(davName)) { String mimeType = IOUtil.getMimeType(value); String encoding = IOUtil.getEncoding(value); contentNode.setProperty(JcrConstants.JCR_MIMETYPE, mimeType); contentNode.setProperty(JcrConstants.JCR_ENCODING, encoding); } else { contentNode.setProperty(getJcrName(davName, contentNode.getSession()), value); } } /** * @param propertyName * @throws RepositoryException */ private void removeJcrProperty(DavPropertyName propertyName, Node contentNode) throws RepositoryException { if (DavPropertyName.GETCONTENTTYPE.equals(propertyName)) { if (contentNode.hasProperty(JcrConstants.JCR_MIMETYPE)) { contentNode.getProperty(JcrConstants.JCR_MIMETYPE).remove(); } if (contentNode.hasProperty(JcrConstants.JCR_ENCODING)) { contentNode.getProperty(JcrConstants.JCR_ENCODING).remove(); } } else { String jcrName = getJcrName(propertyName, contentNode.getSession()); if (contentNode.hasProperty(jcrName)) { contentNode.getProperty(jcrName).remove(); } // removal of non existing property succeeds } } private void setLastModified(Node contentNode, long hint) { try { Calendar lastMod = Calendar.getInstance(); if (hint > IOUtil.UNDEFINED_TIME) { lastMod.setTimeInMillis(hint); } else { lastMod.setTime(new Date()); } contentNode.setProperty(JcrConstants.JCR_LASTMODIFIED, lastMod); } catch (RepositoryException e) { // ignore: property may not be available on the node. // deliberately not rethrowing as IOException. } } private static boolean isDefinedByFilteredNodeType(PropertyDefinition def) { String ntName = def.getDeclaringNodeType().getName(); return ntName.equals(JcrConstants.NT_BASE) || ntName.equals(JcrConstants.MIX_REFERENCEABLE) || ntName.equals(JcrConstants.MIX_VERSIONABLE) || ntName.equals(JcrConstants.MIX_LOCKABLE); } }