diff --git a/config/alfresco/content-services-context.xml b/config/alfresco/content-services-context.xml index bef5ea0518..b5fc56a5a2 100644 --- a/config/alfresco/content-services-context.xml +++ b/config/alfresco/content-services-context.xml @@ -7,6 +7,15 @@ ${dir.contentstore} + + + + + + + diff --git a/config/alfresco/core-services-context.xml b/config/alfresco/core-services-context.xml index 9984231350..d8253d8b57 100644 --- a/config/alfresco/core-services-context.xml +++ b/config/alfresco/core-services-context.xml @@ -36,6 +36,9 @@ classpath:alfresco/repository.properties classpath:alfresco/domain/transaction.properties + + + classpath*:alfresco/enterprise/repository.properties classpath*:alfresco/module/*/alfresco-global.properties @@ -521,6 +524,7 @@ + diff --git a/config/alfresco/messages/action-config_ja.properties b/config/alfresco/messages/action-config_ja.properties index b5ad0e7592..d0a68e6ac2 100755 --- a/config/alfresco/messages/action-config_ja.properties +++ b/config/alfresco/messages/action-config_ja.properties @@ -29,7 +29,7 @@ in-category.description=\u30eb\u30fc\u30eb\u306f\u3001\u6307\u5b9a\u3055\u308c\u in-category.category-aspect.display-label=\u30ab\u30c6\u30b4\u30ea\u306e\u30a2\u30b9\u30da\u30af\u30c8 in-category.category-value.display-label=\u30ab\u30c6\u30b4\u30ea\u306e\u5024 -is-subtype.title=\u30bf\u30a4\u30d7\u307e\u305f\u306f\u30b5\u30d6\u30bf\u30a4\u30d7\u306e\u30b3\u30f3\u30c6\u30f3\u30c4 +is-subtype.title=\u30b3\u30f3\u30c6\u30f3\u30c4\u306e\u30bf\u30a4\u30d7\u307e\u305f\u306f\u30b5\u30d6\u30bf\u30a4\u30d7 is-subtype.description=\u30eb\u30fc\u30eb\u306f\u3001\u6307\u5b9a\u3055\u308c\u305f\u30bf\u30a4\u30d7\u307e\u305f\u306f\u305d\u306e\u30b5\u30d6\u30bf\u30a4\u30d7\u3067\u3042\u308b\u3059\u3079\u3066\u306e\u30a2\u30a4\u30c6\u30e0\u306b\u9069\u7528\u3055\u308c\u307e\u3059\u3002 is-subtype.type.display-label=\u30bf\u30a4\u30d7 diff --git a/config/alfresco/messages/patch-service_es.properties b/config/alfresco/messages/patch-service_es.properties index ca54d72ba8..231ff48423 100755 --- a/config/alfresco/messages/patch-service_es.properties +++ b/config/alfresco/messages/patch-service_es.properties @@ -161,6 +161,8 @@ patch.wcmFolders.webprojects.result.created=The Web Projects folder was successf patch.wcmFolders.webforms.result.exists=The Web Forms folder already exists: {0} patch.wcmFolders.webforms.result.created=The Web Forms folder was successfully created: {0} +patch.wcmDeployed.description=Adds the 'WCM Deployed' space to the company home folder. + patch.linkNodeExtension.description=Fixes link node file extensions to have a .url extension. patch.linkNodeExtension.result=Fixed {0} link node file extensions. See file {1} for details. patch.linkNodeExtension.err.unable_to_fix=Auto-fixing of link node file extensions failed. See file {0} for details. diff --git a/config/alfresco/messages/patch-service_fr.properties b/config/alfresco/messages/patch-service_fr.properties index ca54d72ba8..231ff48423 100755 --- a/config/alfresco/messages/patch-service_fr.properties +++ b/config/alfresco/messages/patch-service_fr.properties @@ -161,6 +161,8 @@ patch.wcmFolders.webprojects.result.created=The Web Projects folder was successf patch.wcmFolders.webforms.result.exists=The Web Forms folder already exists: {0} patch.wcmFolders.webforms.result.created=The Web Forms folder was successfully created: {0} +patch.wcmDeployed.description=Adds the 'WCM Deployed' space to the company home folder. + patch.linkNodeExtension.description=Fixes link node file extensions to have a .url extension. patch.linkNodeExtension.result=Fixed {0} link node file extensions. See file {1} for details. patch.linkNodeExtension.err.unable_to_fix=Auto-fixing of link node file extensions failed. See file {0} for details. diff --git a/config/alfresco/messages/patch-service_zh_CN.properties b/config/alfresco/messages/patch-service_zh_CN.properties index b328dd8a15..231ff48423 100755 --- a/config/alfresco/messages/patch-service_zh_CN.properties +++ b/config/alfresco/messages/patch-service_zh_CN.properties @@ -472,8 +472,3 @@ patch.migrateTenantsFromAttrsToTable.result=Processed {0} tenants patch.remoteCredentialsContainer.description=Patch to add the root folder for Shared Remote Credentials patch.syncSetDefinitionsContainer.description=Patch to add the root folder for SyncSet Definitions - -patch.swsdpPatch.description=Patch to fix up the Sample: Web Site Design Project. -patch.swsdpPatch.success=Successfully patched the Sample: Web Site Design Project. -patch.swsdpPatch.skipped=Skipped, not required. -patch.swsdpPatch.missingSurfConfig=surf-config folder is not present in Sample: Web Site Design Project. \ No newline at end of file diff --git a/config/alfresco/messages/rule-config_ja.properties b/config/alfresco/messages/rule-config_ja.properties index 3e7b3910d5..6ac9e3616c 100755 --- a/config/alfresco/messages/rule-config_ja.properties +++ b/config/alfresco/messages/rule-config_ja.properties @@ -1,6 +1,6 @@ # Rule types -inbound.display-label=\u30a2\u30a4\u30c6\u30e0\u306f\u3053\u306e\u30d5\u30a9\u30eb\u30c0\u3067\u4f5c\u6210\u307e\u305f\u306f\u5165\u529b\u3055\u308c\u307e\u3059\u3002 -outbound.display-label=\u30a2\u30a4\u30c6\u30e0\u306f\u3053\u306e\u30d5\u30a9\u30eb\u30c0\u304b\u3089\u524a\u9664\u307e\u305f\u306f\u6b8b\u3055\u308c\u307e\u3059\u3002 -update.display-label=\u30a2\u30a4\u30c6\u30e0\u304c\u66f4\u65b0\u3055\u308c\u307e\u3059\u3002 +inbound.display-label=\u30a2\u30a4\u30c6\u30e0\u304c\u3053\u306e\u30d5\u30a9\u30eb\u30c0\u306b\u4f5c\u6210\u307e\u305f\u306f\u5165\u529b\u3055\u308c\u305f\u6642 +outbound.display-label=\u30a2\u30a4\u30c6\u30e0\u304c\u3053\u306e\u30d5\u30a9\u30eb\u30c0\u304b\u3089\u524a\u9664\u307e\u305f\u306f\u9001\u308a\u51fa\u3055\u308c\u305f\u6642 +update.display-label=\u30a2\u30a4\u30c6\u30e0\u304c\u66f4\u65b0\u3055\u308c\u305f\u6642 inboundAndUpdate.display-label=\u30a2\u30a4\u30c6\u30e0\u304c\u4f5c\u6210\u3055\u308c\u3001\u3053\u306e\u30d5\u30a9\u30eb\u30c0\u306b\u5165\u308b\u304b\u66f4\u65b0\u3055\u308c\u307e\u3059\u3002 diff --git a/config/alfresco/model/imapModel.xml b/config/alfresco/model/imapModel.xml index b221e081a9..3bc8243383 100644 --- a/config/alfresco/model/imapModel.xml +++ b/config/alfresco/model/imapModel.xml @@ -84,7 +84,7 @@ Attachments Folder false - false + true cm:cmobject diff --git a/config/alfresco/mt/mt-contentstore-context.xml b/config/alfresco/mt/mt-contentstore-context.xml index 9362c1ca1d..014c625d4a 100644 --- a/config/alfresco/mt/mt-contentstore-context.xml +++ b/config/alfresco/mt/mt-contentstore-context.xml @@ -9,6 +9,7 @@ + diff --git a/config/alfresco/public-services-security-context.xml b/config/alfresco/public-services-security-context.xml index 2f6a34a05c..e2c0dfce60 100644 --- a/config/alfresco/public-services-security-context.xml +++ b/config/alfresco/public-services-security-context.xml @@ -1108,27 +1108,6 @@ - - - - - - - - - - - org.alfresco.service.cmr.remoteticket.RemoteAlfrescoTicketService.getRemoteCredentials=ACL_ALLOW - org.alfresco.service.cmr.remoteticket.RemoteAlfrescoTicketService.storeRemoteCredentials=ACL_ALLOW - org.alfresco.service.cmr.remoteticket.RemoteAlfrescoTicketService.deleteRemoteCredentials=ACL_ALLOW - org.alfresco.service.cmr.remoteticket.RemoteAlfrescoTicketService.getAlfrescoTicket=ACL_ALLOW - org.alfresco.service.cmr.remoteticket.RemoteAlfrescoTicketService.refetchAlfrescoTicket=ACL_ALLOW - org.alfresco.service.cmr.remoteticket.RemoteAlfrescoTicketService.registerRemoteSystem=ACL_METHOD.ROLE_ADMINISTRATOR - org.alfresco.service.cmr.remoteticket.RemoteAlfrescoTicketService.*=ACL_DENY - - - - diff --git a/config/alfresco/remote-ticket-services-context.xml b/config/alfresco/remote-ticket-services-context.xml index 5609b4637d..2ae0695324 100644 --- a/config/alfresco/remote-ticket-services-context.xml +++ b/config/alfresco/remote-ticket-services-context.xml @@ -25,7 +25,7 @@ - + @@ -60,6 +60,25 @@ + + + + + + + + org.alfresco.service.cmr.remoteticket.RemoteAlfrescoTicketService.getRemoteCredentials=ACL_ALLOW + org.alfresco.service.cmr.remoteticket.RemoteAlfrescoTicketService.storeRemoteCredentials=ACL_ALLOW + org.alfresco.service.cmr.remoteticket.RemoteAlfrescoTicketService.deleteRemoteCredentials=ACL_ALLOW + org.alfresco.service.cmr.remoteticket.RemoteAlfrescoTicketService.getAlfrescoTicket=ACL_ALLOW + org.alfresco.service.cmr.remoteticket.RemoteAlfrescoTicketService.refetchAlfrescoTicket=ACL_ALLOW + org.alfresco.service.cmr.remoteticket.RemoteAlfrescoTicketService.registerRemoteSystem=ACL_METHOD.ROLE_ADMINISTRATOR + org.alfresco.service.cmr.remoteticket.RemoteAlfrescoTicketService.*=ACL_DENY + + + + + @@ -82,4 +101,5 @@ + diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties index 717fcd29d5..c4cdd10d36 100644 --- a/config/alfresco/repository.properties +++ b/config/alfresco/repository.properties @@ -15,6 +15,13 @@ dir.cachedcontent=${dir.root}/cachedcontent dir.auditcontentstore=${dir.root}/audit.contentstore +# The value for the maximum permitted size in bytes of all content. +# No value (or a negative long) will be taken to mean that no limit should be applied. +# See content-services-context.xml +system.content.maximumFileSizeLimit= + + + # The location for lucene index files dir.indexes=${dir.root}/lucene-indexes @@ -754,6 +761,17 @@ imap.config.server.mountPoints.default.rootPath=${protocols.rootPath} imap.config.server.mountPoints.value.AlfrescoIMAP.mountPointName=Alfresco IMAP imap.config.server.mountPoints.value.AlfrescoIMAP.modeName=MIXED +#Imap extraction settings +#imap.attachments.mode: +# SEPARATE -- All attachments for each email will be extracted to separate folder. +# COMMON -- All attachments for all emails will be extracted to one folder. +# SAME -- Attachments will be extracted to the same folder where email lies. +imap.server.attachments.extraction.enabled=true +imap.attachments.mode=SEPARATE +imap.attachments.folder.store=${spaces.store} +imap.attachments.folder.rootPath=/${spaces.company_home.childname} +imap.attachments.folder.folderPath=Imap Attachments + # Activities Feed - refer to subsystem # Feed max size (number of entries) @@ -983,4 +1001,4 @@ ticket.cleanup.cronExpression=0 0 * * * ? # # Disable load of sample site # -sample.site.disabled=false \ No newline at end of file +sample.site.disabled=false diff --git a/config/alfresco/subsystems/imap/default/imap-server-context.xml b/config/alfresco/subsystems/imap/default/imap-server-context.xml index 1ab31a05b2..c1fdf8b9ad 100644 --- a/config/alfresco/subsystems/imap/default/imap-server-context.xml +++ b/config/alfresco/subsystems/imap/default/imap-server-context.xml @@ -84,6 +84,19 @@ + + + + ${imap.attachments.folder.store} + + + ${imap.attachments.folder.rootPath} + + + ${imap.attachments.folder.folderPath} + + + @@ -95,6 +108,24 @@ + + + + + + + + + + + + + + + ${imap.attachments.mode} + + + @@ -123,6 +154,9 @@ + + + diff --git a/config/alfresco/subsystems/thirdparty/default/swf-transform.properties b/config/alfresco/subsystems/thirdparty/default/swf-transform.properties index e0e2e37aa3..1b103312ee 100644 --- a/config/alfresco/subsystems/thirdparty/default/swf-transform.properties +++ b/config/alfresco/subsystems/thirdparty/default/swf-transform.properties @@ -4,4 +4,4 @@ swf.exe=./bin/pdf2swf # This option on pdf2swf improves the transformation of graphics-heavy pdfs. See ALF-3580. # poly2bitmap improves the chances of successful transformation. On its own it reduces # the resolution of embedded images. subpixels sets the dpi for embedded images. -swf.encoder.params=-s poly2bitmap,subpixels=72 \ No newline at end of file +swf.encoder.params=-s poly2bitmap -s subpixels=72 \ No newline at end of file diff --git a/source/java/org/alfresco/repo/content/AbstractContentStore.java b/source/java/org/alfresco/repo/content/AbstractContentStore.java index b03c706054..49d648171a 100644 --- a/source/java/org/alfresco/repo/content/AbstractContentStore.java +++ b/source/java/org/alfresco/repo/content/AbstractContentStore.java @@ -20,6 +20,7 @@ package org.alfresco.repo.content; import java.util.Date; +import org.alfresco.repo.content.ContentLimitProvider.NoLimitProvider; import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentWriter; @@ -75,6 +76,8 @@ public abstract class AbstractContentStore implements ContentStore } return true; } + + protected ContentLimitProvider contentLimitProvider = new NoLimitProvider(); /** * Splits the content URL into its component parts as separated by @@ -309,4 +312,9 @@ public abstract class AbstractContentStore implements ContentStore { return "."; } + + public void setContentLimitProvider(ContentLimitProvider contentLimitProvider) + { + this.contentLimitProvider = contentLimitProvider; + } } diff --git a/source/java/org/alfresco/repo/content/AbstractContentWriter.java b/source/java/org/alfresco/repo/content/AbstractContentWriter.java index c212e16b86..59fa0c7596 100644 --- a/source/java/org/alfresco/repo/content/AbstractContentWriter.java +++ b/source/java/org/alfresco/repo/content/AbstractContentWriter.java @@ -34,6 +34,7 @@ import java.util.ArrayList; import java.util.List; import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.content.ContentLimitProvider.NoLimitProvider; import org.alfresco.repo.content.encoding.ContentCharsetFinder; import org.alfresco.repo.content.filestore.FileContentWriter; import org.alfresco.service.cmr.repository.ContentAccessor; @@ -67,6 +68,13 @@ public abstract class AbstractContentWriter extends AbstractContentAccessor impl private MimetypeService mimetypeService; private DoGuessingOnCloseListener guessingOnCloseListener; + /** + * This object provides a maximum size limit for content. + * @since Thor + */ + private ContentLimitProvider limitProvider = new NoLimitProvider(); + private LimitedStreamCopier sizeLimitedStreamCopier = new LimitedStreamCopier(); + /** * @param contentUrl the content URL * @param existingContentReader a reader of a previous version of this content @@ -85,6 +93,11 @@ public abstract class AbstractContentWriter extends AbstractContentAccessor impl listeners.add(guessingOnCloseListener); } + public void setContentLimitProvider(ContentLimitProvider limitProvider) + { + this.limitProvider = limitProvider; + } + /** * Supplies the Mimetype Service to be used when guessing * encoding and mimetype information. @@ -164,6 +177,16 @@ public abstract class AbstractContentWriter extends AbstractContentAccessor impl } return reader; } + + /** + * This method returns the configured {@link ContentLimitProvider} for this writer. + * By default a {@link NoLimitProvider} will be returned. + * @since Thor + */ + protected ContentLimitProvider getContentLimitProvider() + { + return this.limitProvider == null ? new NoLimitProvider() : this.limitProvider; + } /** * An automatically created listener sets the flag @@ -470,44 +493,11 @@ public abstract class AbstractContentWriter extends AbstractContentAccessor impl */ private final int copyStreams(InputStream in, OutputStream out) throws IOException { - int byteCount = 0; - IOException error = null; - try - { - byte[] buffer = new byte[4096]; - int bytesRead = -1; - while ((bytesRead = in.read(buffer)) != -1) - { - out.write(buffer, 0, bytesRead); - byteCount += bytesRead; - } - out.flush(); - } - finally - { - try - { - in.close(); - } - catch (IOException e) - { - error = e; - logger.error("Failed to close output stream: " + this, e); - } - try - { - out.close(); - } - catch (IOException e) - { - error = e; - logger.error("Failed to close output stream: " + this, e); - } - } - if (error != null) - { - throw error; - } + ContentLimitProvider contentLimitProvider = getContentLimitProvider(); + final long sizeLimit = contentLimitProvider.getSizeLimit(); + + int byteCount = sizeLimitedStreamCopier.copyStreams(in, out, sizeLimit); + return byteCount; } diff --git a/source/java/org/alfresco/repo/content/ContentLimitProvider.java b/source/java/org/alfresco/repo/content/ContentLimitProvider.java new file mode 100644 index 0000000000..ce8dbdaa55 --- /dev/null +++ b/source/java/org/alfresco/repo/content/ContentLimitProvider.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.content; + +/** + * Implementations of this interface must provide a byte size limit for pieces of Alfresco content. + * Simple implementations of this interface include: + *
    + *
  • {@link NoLimitProvider} which provides a value indicating no limit.
  • + *
  • {@link SimpleFixedLimitProvider} which provides a fixed numerical limit value.
  • + *
+ * It is possible that smarter implementations may be added at a future date. + * + * @author Neil Mc Erlean + * @since Thor + */ +public interface ContentLimitProvider +{ + /** + * A {@link ContentLimitProvider#getSizeLimit() limit} having this value is deemed not to be a limit. + */ + public static final long NO_LIMIT = -1L; + + /** + * This method returns a size limit in bytes. + */ + long getSizeLimit(); + + /** + * A {@link ContentLimitProvider} which returns a fixed value. + */ + public static class SimpleFixedLimitProvider implements ContentLimitProvider + { + private long limit; + + public SimpleFixedLimitProvider() + { + // Default constructor for use as bean. + } + public SimpleFixedLimitProvider(long limit) + { + this.limit = limit; + } + + /** + * This method sets a value for the limit. If the string does not {@link Long#parseLong(String) parse} to a + * java long, the {@link ContentLimitProvider#NO_LIMIT default value} will be applied instead. + * + * @param limit a String representing a valid Java long. + */ + public void setSizeLimitString(String limit) + { + // A string parameter is used here in order to not to require end users to provide a value for the limit in a property + // file. This results in the empty string being injected to this method. + long longLimit = NO_LIMIT; + try + { + longLimit = Long.parseLong(limit); + } catch (NumberFormatException ignored) + { + // Intentionally empty + } + this.limit = longLimit; + } + + @Override public long getSizeLimit() + { + return limit; + } + } + + /** + * A {@link ContentLimitProvider} which returns a value indicating there is no limit. + */ + public static class NoLimitProvider implements ContentLimitProvider + { + @Override public long getSizeLimit() + { + return NO_LIMIT; + } + } +} diff --git a/source/java/org/alfresco/repo/content/ContentLimitViolationException.java b/source/java/org/alfresco/repo/content/ContentLimitViolationException.java new file mode 100644 index 0000000000..38ed88edf0 --- /dev/null +++ b/source/java/org/alfresco/repo/content/ContentLimitViolationException.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.content; + +import org.alfresco.service.cmr.repository.ContentIOException; + +/** + * This exception represents a violation of the defined content limit. + * + * @author Neil Mc Erlean + * @since Thor + */ +public class ContentLimitViolationException extends ContentIOException +{ + private static final long serialVersionUID = -640491905977728606L; + + public ContentLimitViolationException(String msg) + { + super(msg); + } + + public ContentLimitViolationException(String msg, Throwable cause) + { + super(msg, cause); + } +} diff --git a/source/java/org/alfresco/repo/content/LimitedStreamCopier.java b/source/java/org/alfresco/repo/content/LimitedStreamCopier.java new file mode 100644 index 0000000000..d53f161f89 --- /dev/null +++ b/source/java/org/alfresco/repo/content/LimitedStreamCopier.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.content; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This class is a simple utility to copy bytes from an {@link InputStream} to an {@link OutputStream}. + * The copy can be performed with an optional byte limit and as soon as this limit is reached, + * the copy will be stopped immediately and a {@link ContentLimitViolationException} will be thrown. + * + * @since Thor + */ +public final class LimitedStreamCopier +{ + private static final Log logger = LogFactory.getLog(LimitedStreamCopier.class); + private static final int BYTE_BUFFER_SIZE = 4096; + + /** + * Copy of the the Spring FileCopyUtils, but does not silently absorb IOExceptions + * when the streams are closed. We require the stream write to happen successfully. + *

+ * Both streams are closed but any IOExceptions are thrown + * + * @param in the stream from which to read content. + * @param out the stream to which to write content. + * @param sizeLimit the maximum number of bytes that will be copied between the streams before a + * {@link ContentLimitViolationException} will be thrown. + * A negative number or zero will be deemed to mean 'no limit'. + */ + public final int copyStreams(InputStream in, OutputStream out, long sizeLimit) throws IOException + { + int byteCount = 0; + IOException error = null; + + long totalBytesRead = 0; + + try + { + byte[] buffer = new byte[BYTE_BUFFER_SIZE]; + int bytesRead = -1; + while ((bytesRead = in.read(buffer)) != -1) + { + // We are able to abort the copy immediately upon limit violation. + totalBytesRead += bytesRead; + if (sizeLimit > 0 && totalBytesRead > sizeLimit) + { + StringBuilder msg = new StringBuilder(); + msg.append("Content size violation, limit = ") + .append(sizeLimit); + + throw new ContentLimitViolationException(msg.toString()); + } + + out.write(buffer, 0, bytesRead); + byteCount += bytesRead; + } + out.flush(); + } + finally + { + try + { + in.close(); + } + catch (IOException e) + { + error = e; + logger.error("Failed to close output stream: " + this, e); + } + try + { + out.close(); + } + catch (IOException e) + { + error = e; + logger.error("Failed to close output stream: " + this, e); + } + } + if (error != null) + { + throw error; + } + return byteCount; + } +} diff --git a/source/java/org/alfresco/repo/content/LimitedStreamCopierTest.java b/source/java/org/alfresco/repo/content/LimitedStreamCopierTest.java new file mode 100644 index 0000000000..d0a711bf01 --- /dev/null +++ b/source/java/org/alfresco/repo/content/LimitedStreamCopierTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.content; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; + +import org.junit.Before; +import org.junit.Test; + +/** + * Unit test for {@link LimitedStreamCopier}. + * + * @author Neil Mc Erlean + * @since Thor + */ +public class LimitedStreamCopierTest +{ + private final static byte[] ZERO_BYTE_ARRAY = "".getBytes(); + private final static byte[] SHORT_BYTE_ARRAY = "This string is shorter than the limit".getBytes(); + private final static byte[] LIMIT_SIZED_BYTE_ARRAY = "This string's length exactly equals the limit".getBytes(); + private final static byte[] LONG_BYTE_ARRAY = "This test string is longer than the limit. Tum te tum te tum te tum.".getBytes(); + + private final static int SIZE_LIMIT = LIMIT_SIZED_BYTE_ARRAY.length; + + private LimitedStreamCopier streamCopier = new LimitedStreamCopier(); + private InputStream in; + private ByteArrayOutputStream out; + + @Before public void initStreamCopier() + { + out = new ByteArrayOutputStream(); + } + + /** + * Test copying a simple byte[] + */ + @Test public void copyStreamSuccessful() throws Exception + { + in = new ByteArrayInputStream(SHORT_BYTE_ARRAY); + + int bytesCopied = streamCopier.copyStreams(in, out, SIZE_LIMIT); + + assertArrayEquals(SHORT_BYTE_ARRAY, out.toByteArray()); + assertEquals(SHORT_BYTE_ARRAY.length, bytesCopied); + } + + /** + * Test copying an empty byte[] + */ + @Test public void copyStreamSuccessfulZeroBytes() throws Exception + { + in = new ByteArrayInputStream(ZERO_BYTE_ARRAY); + + int bytesCopied = streamCopier.copyStreams(in, out, SIZE_LIMIT); + + assertArrayEquals(ZERO_BYTE_ARRAY, out.toByteArray()); + assertEquals(ZERO_BYTE_ARRAY.length, bytesCopied); + } + + /** + * Test copying a byte[] that exceeds the limit. + */ + @Test (expected=ContentLimitViolationException.class) + public void copyStreamUnsuccessfulLimitExceeded() throws Exception + { + in = new ByteArrayInputStream(LONG_BYTE_ARRAY); + + int bytesCopied = streamCopier.copyStreams(in, out, SIZE_LIMIT); + + assertArrayEquals(LONG_BYTE_ARRAY, out.toByteArray()); + assertEquals(LONG_BYTE_ARRAY.length, bytesCopied); + } + + /** + * Test copying a byte[] that equals the limit. + */ + @Test + public void copyStreamSuccessfulLimitHit() throws Exception + { + in = new ByteArrayInputStream(LIMIT_SIZED_BYTE_ARRAY); + + int bytesCopied = streamCopier.copyStreams(in, out, SIZE_LIMIT); + + assertArrayEquals(LIMIT_SIZED_BYTE_ARRAY, out.toByteArray()); + assertEquals(LIMIT_SIZED_BYTE_ARRAY.length, bytesCopied); + } + + /** + * Test copying a simple byte[] with no limit sent. + */ + @Test public void copyStreamSuccessfulBecauseLimitNotImposed() throws Exception + { + in = new ByteArrayInputStream(LONG_BYTE_ARRAY); + + int bytesCopied = streamCopier.copyStreams(in, out, -1); + + assertArrayEquals(LONG_BYTE_ARRAY, out.toByteArray()); + assertEquals(LONG_BYTE_ARRAY.length, bytesCopied); + } +} diff --git a/source/java/org/alfresco/repo/content/filestore/FileContentStore.java b/source/java/org/alfresco/repo/content/filestore/FileContentStore.java index f9e2cef0e2..05acdc6d29 100644 --- a/source/java/org/alfresco/repo/content/filestore/FileContentStore.java +++ b/source/java/org/alfresco/repo/content/filestore/FileContentStore.java @@ -471,6 +471,11 @@ public class FileContentStore } // create the writer FileContentWriter writer = new FileContentWriter(file, contentUrl, existingContentReader); + + if (contentLimitProvider != null) + { + writer.setContentLimitProvider(contentLimitProvider); + } writer.setAllowRandomAccess(allowRandomAccess); // done diff --git a/source/java/org/alfresco/repo/content/filestore/FileContentStoreTest.java b/source/java/org/alfresco/repo/content/filestore/FileContentStoreTest.java index ecda2aeabd..f47d9db38c 100644 --- a/source/java/org/alfresco/repo/content/filestore/FileContentStoreTest.java +++ b/source/java/org/alfresco/repo/content/filestore/FileContentStoreTest.java @@ -24,6 +24,9 @@ import java.nio.ByteBuffer; import org.alfresco.repo.content.AbstractWritableContentStoreTest; import org.alfresco.repo.content.ContentContext; import org.alfresco.repo.content.ContentExistsException; +import org.alfresco.repo.content.ContentLimitProvider; +import org.alfresco.repo.content.ContentLimitProvider.SimpleFixedLimitProvider; +import org.alfresco.repo.content.ContentLimitViolationException; import org.alfresco.repo.content.ContentStore; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.util.TempFileProvider; @@ -204,6 +207,37 @@ public class FileContentStoreTest extends AbstractWritableContentStoreTest assertDirExists(root, ""); } + /** + * This method tests that writing content with a configured {@link ContentLimitProvider limit} fails with + * the expected exception. + * @since Thor + */ + public void testWriteFileWithSizeLimit() throws Exception + { + ContentWriter writer = getWriter(); + assertEquals("Writer was of wrong type", FileContentWriter.class, writer.getClass()); + + FileContentWriter fileContentWriter = (FileContentWriter)writer; + + // Set a maximum size limit for this writer. We use a limit of 3 bytes. + ContentLimitProvider limitProvider = new SimpleFixedLimitProvider(3); + fileContentWriter.setContentLimitProvider(limitProvider); + + // Attempt to write content that will exceed the limit. + boolean expectedExceptionThrown = false; + try + { + writer.putContent("This will exceed the short limit."); + } + catch (ContentLimitViolationException clvx) + { + expectedExceptionThrown = true; + } + + assertTrue("Expected exception not thrown.", expectedExceptionThrown); + assertTrue("Stream close not detected", writer.isClosed()); + } + private void assertDirExists(File root, String dir) { diff --git a/source/java/org/alfresco/repo/content/transform/OOoContentTransformerHelper.java b/source/java/org/alfresco/repo/content/transform/OOoContentTransformerHelper.java index 115b07324c..06dce6c618 100644 --- a/source/java/org/alfresco/repo/content/transform/OOoContentTransformerHelper.java +++ b/source/java/org/alfresco/repo/content/transform/OOoContentTransformerHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -18,14 +18,94 @@ */ package org.alfresco.repo.content.transform; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; + +import net.sf.jooreports.converter.DocumentFamily; +import net.sf.jooreports.converter.DocumentFormat; +import net.sf.jooreports.converter.DocumentFormatRegistry; +import net.sf.jooreports.converter.XmlDocumentFormatRegistry; +import net.sf.jooreports.openoffice.connection.OpenOfficeException; + +import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.alfresco.service.cmr.repository.TransformationOptions; +import org.alfresco.util.TempFileProvider; +import org.apache.commons.logging.Log; +import org.apache.pdfbox.exceptions.COSVisitorException; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.edit.PDPageContentStream; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; /** * A class providing basic OOo-related functionality shared by both * {@link ContentTransformer}s and {@link ContentTransformerWorker}s. */ -public class OOoContentTransformerHelper extends ContentTransformerHelper +public abstract class OOoContentTransformerHelper extends ContentTransformerHelper { + private String documentFormatsConfiguration; + private DocumentFormatRegistry formatRegistry; + + /** + * Set a non-default location from which to load the document format mappings. + * + * @param path + * a resource location supporting the file: or classpath: prefixes + */ + public void setDocumentFormatsConfiguration(String path) + { + this.documentFormatsConfiguration = path; + } + + protected abstract Log getLogger(); + + protected abstract String getTempFilePrefix(); + + public abstract boolean isAvailable(); + + protected abstract void convert(File tempFromFile, DocumentFormat sourceFormat, File tempToFile, + DocumentFormat targetFormat); + + public void afterPropertiesSet() throws Exception + { + // load the document conversion configuration + if (documentFormatsConfiguration != null) + { + DefaultResourceLoader resourceLoader = new DefaultResourceLoader(); + try + { + InputStream is = resourceLoader.getResource(this.documentFormatsConfiguration).getInputStream(); + formatRegistry = new XmlDocumentFormatRegistry(is); + // We do not need to explicitly close this InputStream as it is closed for us within the XmlDocumentFormatRegistry + } + catch (IOException e) + { + throw new AlfrescoRuntimeException( + "Unable to load document formats configuration file: " + + this.documentFormatsConfiguration); + } + } + else + { + formatRegistry = new XmlDocumentFormatRegistry(); + } + } + /** * There are some conversions that fail, despite the converter believing them possible. * This method can be used by subclasses to check if a targetMimetype or source/target @@ -54,4 +134,290 @@ public class OOoContentTransformerHelper extends ContentTransformerHelper return false; } } + + /** + * @see DocumentFormatRegistry + */ + public boolean isTransformable(String sourceMimetype, String targetMimetype, TransformationOptions options) + { + // Use BinaryPassThroughContentTransformer if mimetypes are the same. + if (sourceMimetype.equals(targetMimetype)) + { + return false; + } + + if (!isAvailable()) + { + // The connection management is must take care of this + return false; + } + + if (isTransformationBlocked(sourceMimetype, targetMimetype)) + { + if (getLogger().isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Transformation from ") + .append(sourceMimetype).append(" to ") + .append(targetMimetype) + .append(" is blocked and therefore unavailable."); + getLogger().debug(msg.toString()); + } + return false; + } + + MimetypeService mimetypeService = getMimetypeService(); + String sourceExtension = mimetypeService.getExtension(sourceMimetype); + String targetExtension = mimetypeService.getExtension(targetMimetype); + // query the registry for the source format + DocumentFormat sourceFormat = formatRegistry.getFormatByFileExtension(sourceExtension); + if (sourceFormat == null) + { + // no document format + return false; + } + // query the registry for the target format + DocumentFormat targetFormat = formatRegistry.getFormatByFileExtension(targetExtension); + if (targetFormat == null) + { + // no document format + return false; + } + + // get the family of the target document + DocumentFamily sourceFamily = sourceFormat.getFamily(); + // does the format support the conversion + if (!targetFormat.isExportableFrom(sourceFamily)) + { + // unable to export from source family of documents to the target format + return false; + } + else + { + return true; + } + } + + /** + * This method produces an empty PDF file at the specified File location. + * Apache's PDFBox is used to create the PDF file. + */ + private void produceEmptyPdfFile(File tempToFile) + { + // If improvement PDFBOX-914 is incorporated, we can do this with a straight call to + // org.apache.pdfbox.TextToPdf.createPDFFromText(new StringReader("")); + // https://issues.apache.org/jira/browse/PDFBOX-914 + + PDDocument pdfDoc = null; + PDPageContentStream contentStream = null; + try + { + pdfDoc = new PDDocument(); + PDPage pdfPage = new PDPage(); + // Even though, we want an empty PDF, some libs (e.g. PDFRenderer) object to PDFs + // that have literally nothing in them. So we'll put a content stream in it. + contentStream = new PDPageContentStream(pdfDoc, pdfPage); + pdfDoc.addPage(pdfPage); + + // Now write the in-memory PDF document into the temporary file. + pdfDoc.save(tempToFile.getAbsolutePath()); + + } + catch (COSVisitorException cvx) + { + throw new ContentIOException("Error creating empty PDF file", cvx); + } + catch (IOException iox) + { + throw new ContentIOException("Error creating empty PDF file", iox); + } + finally + { + if (contentStream != null) + { + try + { + contentStream.close(); + } + catch (IOException ignored) + { + // Intentionally empty + } + } + if (pdfDoc != null) + { + try + { + pdfDoc.close(); + } + catch (IOException ignored) + { + // Intentionally empty. + } + } + } + } + + /* + * (non-Javadoc) + * @see org.alfresco.repo.content.transform.ContentTransformerWorker#getVersionString() + */ + public String getVersionString() + { + return ""; + } + + public void transform( + ContentReader reader, + ContentWriter writer, + TransformationOptions options) throws Exception + { + if (isAvailable() == false) + { + throw new ContentIOException("Content conversion failed (unavailable): \n" + + " reader: " + reader + "\n" + + " writer: " + writer); + } + + if (getLogger().isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("transforming content from ") + .append(reader.getMimetype()) + .append(" to ") + .append(writer.getMimetype()); + getLogger().debug(msg.toString()); + } + + String sourceMimetype = getMimetype(reader); + String targetMimetype = getMimetype(writer); + + MimetypeService mimetypeService = getMimetypeService(); + String sourceExtension = mimetypeService.getExtension(sourceMimetype); + String targetExtension = mimetypeService.getExtension(targetMimetype); + // query the registry for the source format + DocumentFormat sourceFormat = formatRegistry.getFormatByFileExtension(sourceExtension); + if (sourceFormat == null) + { + // source format is not recognised + throw new ContentIOException("No OpenOffice document format for source extension: " + sourceExtension); + } + // query the registry for the target format + DocumentFormat targetFormat = formatRegistry.getFormatByFileExtension(targetExtension); + if (targetFormat == null) + { + // target format is not recognised + throw new ContentIOException("No OpenOffice document format for target extension: " + targetExtension); + } + // get the family of the target document + DocumentFamily sourceFamily = sourceFormat.getFamily(); + // does the format support the conversion + if (!targetFormat.isExportableFrom(sourceFamily)) + { + throw new ContentIOException( + "OpenOffice conversion not supported: \n" + + " reader: " + reader + "\n" + + " writer: " + writer); + } + + // create temporary files to convert from and to + File tempFromFile = TempFileProvider.createTempFile( + getTempFilePrefix()+"-source-", + "." + sourceExtension); + File tempToFile = TempFileProvider.createTempFile( + getTempFilePrefix()+"-target-", + "." + targetExtension); + + // There is a bug (reported in ALF-219) whereby JooConverter (the Alfresco Community Edition's 3rd party + // OpenOffice connector library) struggles to handle zero-size files being transformed to pdf. + // For zero-length .html files, it throws NullPointerExceptions. + // For zero-length .txt files, it produces a pdf transformation, but it is not a conformant + // pdf file and cannot be viewed (contains no pages). + // + // For these reasons, if the file is of zero length, we will not use JooConverter & OpenOffice + // and will instead ask Apache PDFBox to produce an empty pdf file for us. + final long documentSize = reader.getSize(); + if (documentSize == 0L) + { + produceEmptyPdfFile(tempToFile); + } + else + { + // download the content from the source reader + saveContentInFile(sourceMimetype, reader, tempFromFile); + + // We have some content, so we'll use OpenOffice to render the pdf document. + // Currently, OpenOffice does a better job of rendering documents into PDF and so + // it is preferred over PDFBox. + try + { + convert(tempFromFile, sourceFormat, tempToFile, targetFormat); + } + catch (OpenOfficeException e) + { + throw new ContentIOException("OpenOffice server conversion failed: \n" + + " reader: " + reader + "\n" + + " writer: " + writer + "\n" + + " from file: " + tempFromFile + "\n" + + " to file: " + tempToFile, + e); + } + } + + // upload the temp output to the writer given us + writer.putContent(tempToFile); + + if (getLogger().isDebugEnabled()) + { + getLogger().debug("transformation successful"); + } + } + + /** + * Populates a file with the content in the reader. + */ + public void saveContentInFile(String sourceMimetype, ContentReader reader, File file) throws ContentIOException + { + String encoding = reader.getEncoding(); + if (encodeAsUtf8(sourceMimetype, encoding)) + { + saveContentInUtf8File(reader, file); + } + else + { + reader.getContent(file); + } + } + + /** + * Returns {@code true} if the input file should be transformed to UTF8 encoding.

+ * + * OpenOffice/LibreOffice is unable to support the import of text files that are SHIFT JIS encoded + * (and others: windows-1252...) so transformed to UTF-8. + */ + protected boolean encodeAsUtf8(String sourceMimetype, String encoding) + { + return MimetypeMap.MIMETYPE_TEXT_PLAIN.equals(sourceMimetype) && !"UTF-8".equals(encoding); + } + + /** + * Populates a file with the content in the reader, but also converts the encoding to UTF-8. + */ + private void saveContentInUtf8File(ContentReader reader, File file) + { + String encoding = reader.getEncoding(); + try + { + Reader in = new InputStreamReader(reader.getContentInputStream(), encoding); + Writer out = new OutputStreamWriter(new BufferedOutputStream(new FileOutputStream(file)), "UTF-8"); + + FileCopyUtils.copy(in, out); // both streams are closed + } + catch (IOException e) + { + throw new ContentIOException("Failed to copy content to file and convert "+encoding+" to UTF-8: \n" + + " file: " + file, + e); + } + } } \ No newline at end of file diff --git a/source/java/org/alfresco/repo/content/transform/OpenOfficeContentTransformerWorker.java b/source/java/org/alfresco/repo/content/transform/OpenOfficeContentTransformerWorker.java index fd5343c245..6b40421c9f 100644 --- a/source/java/org/alfresco/repo/content/transform/OpenOfficeContentTransformerWorker.java +++ b/source/java/org/alfresco/repo/content/transform/OpenOfficeContentTransformerWorker.java @@ -19,34 +19,20 @@ package org.alfresco.repo.content.transform; import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import net.sf.jooreports.converter.DocumentFamily; import net.sf.jooreports.converter.DocumentFormat; -import net.sf.jooreports.converter.DocumentFormatRegistry; -import net.sf.jooreports.converter.XmlDocumentFormatRegistry; import net.sf.jooreports.openoffice.connection.OpenOfficeConnection; -import net.sf.jooreports.openoffice.connection.OpenOfficeException; import net.sf.jooreports.openoffice.converter.AbstractOpenOfficeDocumentConverter; import net.sf.jooreports.openoffice.converter.OpenOfficeDocumentConverter; -import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.content.MimetypeMap; import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentReader; -import org.alfresco.service.cmr.repository.ContentWriter; -import org.alfresco.service.cmr.repository.MimetypeService; -import org.alfresco.service.cmr.repository.TransformationOptions; import org.alfresco.util.PropertyCheck; import org.alfresco.util.SocketOpenOfficeConnection; -import org.alfresco.util.TempFileProvider; -import org.alfresco.util.bean.BooleanBean; -import org.apache.pdfbox.exceptions.COSVisitorException; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.edit.PDPageContentStream; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.InitializingBean; -import org.springframework.core.io.DefaultResourceLoader; /** * Makes use of the {@link http://sourceforge.net/projects/joott/JOOConverter} library to perform @@ -58,10 +44,10 @@ import org.springframework.core.io.DefaultResourceLoader; */ public class OpenOfficeContentTransformerWorker extends OOoContentTransformerHelper implements ContentTransformerWorker, InitializingBean { + private static Log logger = LogFactory.getLog(OpenOfficeContentTransformerWorker.class); + private OpenOfficeConnection connection; private AbstractOpenOfficeDocumentConverter converter; - private String documentFormatsConfiguration; - private DocumentFormatRegistry formatRegistry; /** * @param connection @@ -86,46 +72,30 @@ public class OpenOfficeContentTransformerWorker extends OOoContentTransformerHel this.converter = converter; } - /** - * Set a non-default location from which to load the document format mappings. - * - * @param path - * a resource location supporting the file: or classpath: prefixes - */ - public void setDocumentFormatsConfiguration(String path) + @Override + protected Log getLogger() { - this.documentFormatsConfiguration = path; + return logger; } + @Override + protected String getTempFilePrefix() + { + return "OpenOfficeContentTransformer"; + } + + @Override public boolean isAvailable() { return connection.isConnected(); } + @Override public void afterPropertiesSet() throws Exception { PropertyCheck.mandatory("OpenOfficeContentTransformerWorker", "connection", connection); - // load the document conversion configuration - if (documentFormatsConfiguration != null) - { - DefaultResourceLoader resourceLoader = new DefaultResourceLoader(); - try - { - InputStream is = resourceLoader.getResource(documentFormatsConfiguration).getInputStream(); - formatRegistry = new XmlDocumentFormatRegistry(is); - } - catch (IOException e) - { - throw new AlfrescoRuntimeException( - "Unable to load document formats configuration file: " + - documentFormatsConfiguration); - } - } - else - { - formatRegistry = new XmlDocumentFormatRegistry(); - } + super.afterPropertiesSet(); // set up the converter if (converter == null) @@ -136,220 +106,25 @@ public class OpenOfficeContentTransformerWorker extends OOoContentTransformerHel protected AbstractOpenOfficeDocumentConverter getDefaultConverter(OpenOfficeConnection connection) { - return (connection instanceof SocketOpenOfficeConnection) + return (connection instanceof SocketOpenOfficeConnection) ? ((SocketOpenOfficeConnection)connection).getDefaultConverter() : new OpenOfficeDocumentConverter(connection); } - /** - * @see DocumentFormatRegistry - */ - public boolean isTransformable(String sourceMimetype, String targetMimetype, TransformationOptions options) + @Override + protected void convert(File tempFromFile, DocumentFormat sourceFormat, File tempToFile, + DocumentFormat targetFormat) { - // Use BinaryPassThroughContentTransformer if mimetypes are the same. - if (sourceMimetype.equals(targetMimetype)) - { - return false; - } - - if (!isAvailable()) - { - // The connection management is must take care of this - return false; - } - - if (isTransformationBlocked(sourceMimetype, targetMimetype)) - { - return false; - } - - MimetypeService mimetypeService = getMimetypeService(); - String sourceExtension = mimetypeService.getExtension(sourceMimetype); - String targetExtension = mimetypeService.getExtension(targetMimetype); - // query the registry for the source format - DocumentFormat sourceFormat = formatRegistry.getFormatByFileExtension(sourceExtension); - if (sourceFormat == null) - { - // no document format - return false; - } - // query the registry for the target format - DocumentFormat targetFormat = formatRegistry.getFormatByFileExtension(targetExtension); - if (targetFormat == null) - { - // no document format - return false; - } - - // get the family of the target document - DocumentFamily sourceFamily = sourceFormat.getFamily(); - // does the format support the conversion - if (!targetFormat.isExportableFrom(sourceFamily)) - { - // unable to export from source family of documents to the target format - return false; - } - else - { - return true; - } + converter.convert(tempFromFile, sourceFormat, tempToFile, targetFormat); } - - public void transform( - ContentReader reader, - ContentWriter writer, - TransformationOptions options) throws Exception + + public void saveContentInFile(String sourceMimetype, ContentReader reader, File file) throws ContentIOException { - String sourceMimetype = getMimetype(reader); - String targetMimetype = getMimetype(writer); - - MimetypeService mimetypeService = getMimetypeService(); - String sourceExtension = mimetypeService.getExtension(sourceMimetype); - String targetExtension = mimetypeService.getExtension(targetMimetype); - // query the registry for the source format - DocumentFormat sourceFormat = formatRegistry.getFormatByFileExtension(sourceExtension); - if (sourceFormat == null) - { - // source format is not recognised - throw new ContentIOException("No OpenOffice document format for source extension: " + sourceExtension); - } - // query the registry for the target format - DocumentFormat targetFormat = formatRegistry.getFormatByFileExtension(targetExtension); - if (targetFormat == null) - { - // target format is not recognised - throw new ContentIOException("No OpenOffice document format for target extension: " + targetExtension); - } - // get the family of the target document - DocumentFamily sourceFamily = sourceFormat.getFamily(); - // does the format support the conversion - if (!targetFormat.isExportableFrom(sourceFamily)) - { - throw new ContentIOException( - "OpenOffice conversion not supported: \n" + - " reader: " + reader + "\n" + - " writer: " + writer); - } - - // create temporary files to convert from and to - File tempFromFile = TempFileProvider.createTempFile( - "OpenOfficeContentTransformer-source-", - "." + sourceExtension); - File tempToFile = TempFileProvider.createTempFile( - "OpenOfficeContentTransformer-target-", - "." + targetExtension); - + // jooconverter does not handle non western chars by default in text files. + // Jodconverter does by setting properties it passes to soffice. + // The following patched method added to jooconverter, addes these properties. + converter.setTextUtf8(MimetypeMap.MIMETYPE_TEXT_PLAIN.equals(sourceMimetype)); - final long documentSize = reader.getSize(); - - // download the content from the source reader - reader.getContent(tempFromFile); - - // There is a bug (reported in ALF-219) whereby JooConverter (the Alfresco Community Edition's 3rd party - // OpenOffice connector library) struggles to handle zero-size files being transformed to pdf. - // For zero-length .html files, it throws NullPointerExceptions. - // For zero-length .txt files, it produces a pdf transformation, but it is not a conformant - // pdf file and cannot be viewed (contains no pages). - // - // For these reasons, if the file is of zero length, we will not use JooConverter & OpenOffice - // and will instead ask Apache PDFBox to produce an empty pdf file for us. - if (documentSize == 0L) - { - produceEmptyPdfFile(tempToFile); - } - else - { - // We have some content, so we'll use OpenOffice to render the pdf document. - // Currently, OpenOffice does a better job of rendering documents into PDF and so - // it is preferred over PDFBox. - try - { - converter.convert(tempFromFile, sourceFormat, tempToFile, targetFormat); - // conversion success - } - catch (OpenOfficeException e) - { - throw new ContentIOException("OpenOffice server conversion failed: \n" + - " reader: " + reader + "\n" + - " writer: " + writer + "\n" + - " from file: " + tempFromFile + "\n" + - " to file: " + tempToFile, - e); - } - } - - - // upload the temp output to the writer given us - writer.putContent(tempToFile); - } - - /** - * This method produces an empty PDF file at the specified File location. - * Apache's PDFBox is used to create the PDF file. - */ - private void produceEmptyPdfFile(File tempToFile) - { - // If improvement PDFBOX-914 is incorporated, we can do this with a straight call to - // org.apache.pdfbox.TextToPdf.createPDFFromText(new StringReader("")); - // https://issues.apache.org/jira/browse/PDFBOX-914 - - PDDocument pdfDoc = null; - PDPageContentStream contentStream = null; - try - { - pdfDoc = new PDDocument(); - PDPage pdfPage = new PDPage(); - // Even though, we want an empty PDF, some libs (e.g. PDFRenderer) object to PDFs - // that have literally nothing in them. So we'll put a content stream in it. - contentStream = new PDPageContentStream(pdfDoc, pdfPage); - pdfDoc.addPage(pdfPage); - - // Now write the in-memory PDF document into the temporary file. - pdfDoc.save(tempToFile.getAbsolutePath()); - - } - catch (COSVisitorException cvx) - { - throw new ContentIOException("Error creating empty PDF file", cvx); - } - catch (IOException iox) - { - throw new ContentIOException("Error creating empty PDF file", iox); - } - finally - { - if (contentStream != null) - { - try - { - contentStream.close(); - } - catch (IOException ignored) - { - // Intentionally empty - } - } - if (pdfDoc != null) - { - try - { - pdfDoc.close(); - } - catch (IOException ignored) - { - // Intentionally empty. - } - } - } - } - - /* - * (non-Javadoc) - * @see org.alfresco.repo.content.transform.ContentTransformerWorker#getVersionString() - */ - public String getVersionString() - { - // Actual version information owned by OpenOfficeConnectionTester - return ""; + super.saveContentInFile(sourceMimetype, reader, file); } } diff --git a/source/java/org/alfresco/repo/descriptor/DescriptorServiceImpl.java b/source/java/org/alfresco/repo/descriptor/DescriptorServiceImpl.java index f6797b4d97..edb558f05d 100644 --- a/source/java/org/alfresco/repo/descriptor/DescriptorServiceImpl.java +++ b/source/java/org/alfresco/repo/descriptor/DescriptorServiceImpl.java @@ -19,6 +19,8 @@ package org.alfresco.repo.descriptor; import java.lang.reflect.Constructor; +import java.util.HashSet; +import java.util.Set; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.repo.security.authentication.AuthenticationUtil; @@ -240,6 +242,10 @@ public class DescriptorServiceImpl extends AbstractLifecycleBean // Load heart-beat special service (even if disabled at the moment) heartBeat = constructSpecialService("org.alfresco.enterprise.heartbeat.HeartBeat"); + for(LicenseChangeHandler handler : deferredHandlers) + { + licenseService.registerOnLicenseChange(handler); + } // Now listen for future license changes licenseService.registerOnLicenseChange(this); @@ -671,4 +677,19 @@ public class DescriptorServiceImpl extends AbstractLifecycleBean LicenseMode.UNKNOWN); } } + + + Set deferredHandlers = new HashSet(); + @Override + public void registerOnLicenseChange(LicenseChangeHandler callback) + { + if(licenseService != null) + { + licenseService.registerOnLicenseChange(callback); + } + else + { + deferredHandlers.add(callback); + } + } } diff --git a/source/java/org/alfresco/repo/imap/AlfrescoImapFolder.java b/source/java/org/alfresco/repo/imap/AlfrescoImapFolder.java index 51b0db52b4..3ec0e7b6c7 100644 --- a/source/java/org/alfresco/repo/imap/AlfrescoImapFolder.java +++ b/source/java/org/alfresco/repo/imap/AlfrescoImapFolder.java @@ -771,7 +771,7 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab if (extractAttachmentsEnabled) { - imapService.extractAttachments(folderFileInfo.getNodeRef(), messageFile.getNodeRef(), message); + imapService.extractAttachments(messageFile.getNodeRef(), message); } // Force persistence of the message to the repository new IncomingImapMessage(messageFile, serviceRegistry, message); diff --git a/source/java/org/alfresco/repo/imap/AttachmentsExtractor.java b/source/java/org/alfresco/repo/imap/AttachmentsExtractor.java new file mode 100644 index 0000000000..892a0a5a6f --- /dev/null +++ b/source/java/org/alfresco/repo/imap/AttachmentsExtractor.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.imap; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; + +import javax.mail.MessagingException; +import javax.mail.Multipart; +import javax.mail.Part; +import javax.mail.internet.ContentType; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeUtility; + +import org.alfresco.model.ContentModel; +import org.alfresco.model.ImapModel; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.util.config.RepositoryFolderConfigBean; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.util.FileCopyUtils; + +/** + * Extract attachments according to provided AttachmentsExtractorMode + * + * @since 3.4.7 + */ +public class AttachmentsExtractor +{ + public static enum AttachmentsExtractorMode + { + /** + * Attachments will be extracted to the same folder where email lies. + */ + SAME, + /** + * All attachments for all emails will be extracted to one folder. + */ + COMMON, + /** + * All attachments for each email will be extracted to separate folder. + */ + SEPARATE + } + + private Log logger = LogFactory.getLog(AttachmentsExtractor.class); + + private FileFolderService fileFolderService; + private NodeService nodeService; + private ServiceRegistry serviceRegistry; + private RepositoryFolderConfigBean attachmentsFolder; + private NodeRef attachmentsFolderRef; + private AttachmentsExtractorMode attachmentsExtractorMode; + + public void setFileFolderService(FileFolderService fileFolderService) + { + this.fileFolderService = fileFolderService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setAttachmentsFolder(RepositoryFolderConfigBean attachmentsFolder) + { + this.attachmentsFolder = attachmentsFolder; + } + + public void setServiceRegistry(ServiceRegistry serviceRegistry) + { + this.serviceRegistry = serviceRegistry; + } + + public void setAttachmentsExtractorMode(String attachmentsExtractorMode) + { + this.attachmentsExtractorMode = AttachmentsExtractorMode.valueOf(attachmentsExtractorMode); + } + + public void init() + { + attachmentsFolderRef = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public NodeRef doWork() throws Exception + { + NodeRef attFolderRef = attachmentsFolder.getOrCreateFolderPath(serviceRegistry.getNamespaceService(), nodeService, serviceRegistry.getSearchService(), fileFolderService); + serviceRegistry.getPermissionService().setPermission(attFolderRef , PermissionService.ALL_AUTHORITIES, PermissionService.FULL_CONTROL, true); + return attFolderRef; + } + }, AuthenticationUtil.getSystemUserName()); + } + + public void extractAttachments(NodeRef messageRef, MimeMessage originalMessage) throws IOException, MessagingException + { + NodeRef attachmentsFolderRef = null; + switch (attachmentsExtractorMode) + { + case SAME: + attachmentsFolderRef = nodeService.getPrimaryParent(messageRef).getParentRef(); + break; + case COMMON: + attachmentsFolderRef = this.attachmentsFolderRef; + break; + case SEPARATE: + default: + NodeRef parentFolder = nodeService.getPrimaryParent(messageRef).getParentRef(); + String messageName = (String) nodeService.getProperty(messageRef, ContentModel.PROP_NAME); + String attachmentsFolderName = messageName + "-attachments"; + attachmentsFolderRef = fileFolderService.create(parentFolder, attachmentsFolderName, ContentModel.TYPE_FOLDER).getNodeRef(); + break; + } + + nodeService.createAssociation(messageRef, attachmentsFolderRef, ImapModel.ASSOC_IMAP_ATTACHMENTS_FOLDER); + + Object content = originalMessage.getContent(); + if (content instanceof Multipart) + { + Multipart multipart = (Multipart) content; + + for (int i = 0, n = multipart.getCount(); i < n; i++) + { + Part part = multipart.getBodyPart(i); + if ("attachment".equalsIgnoreCase(part.getDisposition())) + { + createAttachment(messageRef, attachmentsFolderRef, part); + } + } + } + + } + + private void createAttachment(NodeRef messageFile, NodeRef attachmentsFolderRef, Part part) throws MessagingException, IOException + { + String fileName = part.getFileName(); + try + { + fileName = MimeUtility.decodeText(fileName); + } + catch (UnsupportedEncodingException e) + { + if (logger.isWarnEnabled()) + { + logger.warn("Cannot decode file name '" + fileName + "'", e); + } + } + + ContentType contentType = new ContentType(part.getContentType()); + NodeRef attachmentFile = fileFolderService.searchSimple(attachmentsFolderRef, fileName); + // The one possible behaviour + /* + if (result.size() > 0) + { + for (FileInfo fi : result) + { + fileFolderService.delete(fi.getNodeRef()); + } + } + */ + // And another one behaviour which will overwrite the content of the existing file. It is performance preferable. + if (attachmentFile == null) + { + FileInfo createdFile = fileFolderService.create(attachmentsFolderRef, fileName, ContentModel.TYPE_CONTENT); + nodeService.createAssociation(messageFile, createdFile.getNodeRef(), ImapModel.ASSOC_IMAP_ATTACHMENT); + attachmentFile = createdFile.getNodeRef(); + } + else + { + String name = fileName; + String ext = ""; + if (fileName.lastIndexOf(".") != -1) + { + int index = fileName.lastIndexOf("."); + name = fileName.substring(0, index); + ext = fileName.substring(index); + + } + + int copyNum = 0; + do + { + copyNum++; + } while (fileFolderService.searchSimple(attachmentsFolderRef, name + " (" + copyNum + ")" + ext) != null); + + FileInfo createdFile = fileFolderService.create(attachmentsFolderRef, name + " (" + copyNum + ")" + ext, ContentModel.TYPE_CONTENT); + nodeService.createAssociation(messageFile, createdFile.getNodeRef(), ImapModel.ASSOC_IMAP_ATTACHMENT); + attachmentFile = createdFile.getNodeRef(); + + } + + nodeService.setProperty(attachmentFile, ContentModel.PROP_DESCRIPTION, nodeService.getProperty(messageFile, ContentModel.PROP_NAME)); + + ContentWriter writer = fileFolderService.getWriter(attachmentFile); + writer.setMimetype(contentType.getBaseType()); + OutputStream os = writer.getContentOutputStream(); + FileCopyUtils.copy(part.getInputStream(), os); + } + +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/imap/ImapService.java b/source/java/org/alfresco/repo/imap/ImapService.java index 4a666bc795..30ebddc27a 100644 --- a/source/java/org/alfresco/repo/imap/ImapService.java +++ b/source/java/org/alfresco/repo/imap/ImapService.java @@ -305,6 +305,16 @@ public interface ImapService */ public boolean getImapServerEnabled(); + /** + * Extract attachments from message. + * + * @param messageRef nodeRef that represents message in Alfresco. + * @param originalMessage original message in eml format. + * @throws IOException + * @throws MessagingException + */ + public void extractAttachments(NodeRef messageRef, MimeMessage originalMessage) throws IOException, MessagingException; + static class FolderStatus { public final int messageCount; diff --git a/source/java/org/alfresco/repo/imap/ImapServiceImpl.java b/source/java/org/alfresco/repo/imap/ImapServiceImpl.java index 849dcacc24..a7a826598a 100644 --- a/source/java/org/alfresco/repo/imap/ImapServiceImpl.java +++ b/source/java/org/alfresco/repo/imap/ImapServiceImpl.java @@ -146,6 +146,7 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol private MimetypeService mimetypeService; private NamespaceService namespaceService; private SearchService searchService; + private AttachmentsExtractor attachmentsExtractor; // Note that this cache need not be cluster synchronized, as it is keyed by the cluster-safe // change token. Key is username, changeToken @@ -271,6 +272,12 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol this.policyBehaviourFilter = policyFilter; } + + public void setAttachmentsExtractor(AttachmentsExtractor attachmentsExtractor) + { + this.attachmentsExtractor = attachmentsExtractor; + } + public void setImapHome(RepositoryFolderConfigBean imapHomeConfigBean) { this.imapHomeConfigBean = imapHomeConfigBean; @@ -2091,6 +2098,11 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol return searchService; } + public void extractAttachments(NodeRef messageRef, MimeMessage originalMessage) throws IOException, MessagingException + { + attachmentsExtractor.extractAttachments(messageRef, originalMessage); + } + static class CacheItem { private Date modified; diff --git a/source/java/org/alfresco/repo/lock/LockServiceImpl.java b/source/java/org/alfresco/repo/lock/LockServiceImpl.java index c4102635fc..3ef4961a80 100644 --- a/source/java/org/alfresco/repo/lock/LockServiceImpl.java +++ b/source/java/org/alfresco/repo/lock/LockServiceImpl.java @@ -35,6 +35,7 @@ import org.alfresco.repo.copy.CopyDetails; import org.alfresco.repo.copy.CopyServicePolicies; import org.alfresco.repo.copy.DefaultCopyBehaviourCallback; import org.alfresco.repo.node.NodeServicePolicies; +import org.alfresco.repo.policy.BehaviourFilter; import org.alfresco.repo.policy.JavaBehaviour; import org.alfresco.repo.policy.PolicyComponent; import org.alfresco.repo.policy.PolicyScope; @@ -57,6 +58,7 @@ import org.alfresco.service.cmr.search.ResultSet; import org.alfresco.service.cmr.search.SearchService; import org.alfresco.service.cmr.security.AuthenticationService; import org.alfresco.service.namespace.QName; +import org.alfresco.util.PropertyCheck; /** * Simple Lock service implementation @@ -79,6 +81,7 @@ public class LockServiceImpl implements LockService, private TenantService tenantService; private AuthenticationService authenticationService; private SearchService searchService; + private BehaviourFilter behaviourFilter; private PolicyComponent policyComponent; @@ -112,6 +115,13 @@ public class LockServiceImpl implements LockService, */ public void init() { + PropertyCheck.mandatory(this, "nodeService", nodeService); + PropertyCheck.mandatory(this, "tenantService", tenantService); + PropertyCheck.mandatory(this, "authenticationService", authenticationService); + PropertyCheck.mandatory(this, "searchService", searchService); + PropertyCheck.mandatory(this, "behaviourFilter", behaviourFilter); + PropertyCheck.mandatory(this, "policyComponent", policyComponent); + // Register the various class behaviours to enable lock checking this.policyComponent.bindAssociationBehaviour( NodeServicePolicies.OnCreateChildAssociationPolicy.QNAME, @@ -649,4 +659,26 @@ public class LockServiceImpl implements LockService, NodeRef nodeRef = oldChildAssocRef.getChildRef(); checkForLock(nodeRef); } + + @Override + public void suspendLocks() + { + getBehaviourFilter().disableBehaviour(ContentModel.ASPECT_LOCKABLE); + } + + @Override + public void enableLocks() + { + getBehaviourFilter().enableBehaviour(ContentModel.ASPECT_LOCKABLE); + } + + public void setBehaviourFilter(BehaviourFilter behaviourFilter) + { + this.behaviourFilter = behaviourFilter; + } + + public BehaviourFilter getBehaviourFilter() + { + return behaviourFilter; + } } diff --git a/source/java/org/alfresco/repo/node/AbstractNodeServiceImpl.java b/source/java/org/alfresco/repo/node/AbstractNodeServiceImpl.java index 6819d48fa1..cd83b4459e 100644 --- a/source/java/org/alfresco/repo/node/AbstractNodeServiceImpl.java +++ b/source/java/org/alfresco/repo/node/AbstractNodeServiceImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -32,6 +32,7 @@ import org.alfresco.repo.node.NodeServicePolicies.BeforeCreateNodePolicy; import org.alfresco.repo.node.NodeServicePolicies.BeforeCreateStorePolicy; import org.alfresco.repo.node.NodeServicePolicies.BeforeDeleteChildAssociationPolicy; import org.alfresco.repo.node.NodeServicePolicies.BeforeDeleteNodePolicy; +import org.alfresco.repo.node.NodeServicePolicies.BeforeMoveNodePolicy; import org.alfresco.repo.node.NodeServicePolicies.BeforeRemoveAspectPolicy; import org.alfresco.repo.node.NodeServicePolicies.BeforeSetNodeTypePolicy; import org.alfresco.repo.node.NodeServicePolicies.BeforeUpdateNodePolicy; @@ -108,6 +109,7 @@ public abstract class AbstractNodeServiceImpl implements NodeService private ClassPolicyDelegate onCreateStoreDelegate; private ClassPolicyDelegate beforeCreateNodeDelegate; private ClassPolicyDelegate onCreateNodeDelegate; + private ClassPolicyDelegate beforeMoveNodeDelegate; private ClassPolicyDelegate onMoveNodeDelegate; private ClassPolicyDelegate beforeUpdateNodeDelegate; private ClassPolicyDelegate onUpdateNodeDelegate; @@ -198,6 +200,7 @@ public abstract class AbstractNodeServiceImpl implements NodeService onCreateStoreDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.OnCreateStorePolicy.class); beforeCreateNodeDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.BeforeCreateNodePolicy.class); onCreateNodeDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.OnCreateNodePolicy.class); + beforeMoveNodeDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.BeforeMoveNodePolicy.class); onMoveNodeDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.OnMoveNodePolicy.class); beforeUpdateNodeDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.BeforeUpdateNodePolicy.class); onUpdateNodeDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.OnUpdateNodePolicy.class); @@ -207,18 +210,18 @@ public abstract class AbstractNodeServiceImpl implements NodeService beforeDeleteNodeDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.BeforeDeleteNodePolicy.class); onDeleteNodeDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.OnDeleteNodePolicy.class); onRestoreNodePolicy = policyComponent.registerClassPolicy(NodeServicePolicies.OnRestoreNodePolicy.class); - + beforeAddAspectDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.BeforeAddAspectPolicy.class); onAddAspectDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.OnAddAspectPolicy.class); beforeRemoveAspectDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.BeforeRemoveAspectPolicy.class); onRemoveAspectDelegate = policyComponent.registerClassPolicy(NodeServicePolicies.OnRemoveAspectPolicy.class); - + beforeCreateNodeAssociationDelegate = policyComponent.registerAssociationPolicy(NodeServicePolicies.BeforeCreateNodeAssociationPolicy.class); onCreateNodeAssociationDelegate = policyComponent.registerAssociationPolicy(NodeServicePolicies.OnCreateNodeAssociationPolicy.class); onCreateChildAssociationDelegate = policyComponent.registerAssociationPolicy(NodeServicePolicies.OnCreateChildAssociationPolicy.class); beforeDeleteChildAssociationDelegate = policyComponent.registerAssociationPolicy(NodeServicePolicies.BeforeDeleteChildAssociationPolicy.class); onDeleteChildAssociationDelegate = policyComponent.registerAssociationPolicy(NodeServicePolicies.OnDeleteChildAssociationPolicy.class); - + onCreateAssociationDelegate = policyComponent.registerAssociationPolicy(NodeServicePolicies.OnCreateAssociationPolicy.class); onDeleteAssociationDelegate = policyComponent.registerAssociationPolicy(NodeServicePolicies.OnDeleteAssociationPolicy.class); } @@ -299,7 +302,26 @@ public abstract class AbstractNodeServiceImpl implements NodeService NodeServicePolicies.OnCreateNodePolicy policy = onCreateNodeDelegate.get(childNodeRef, qnames); policy.onCreateNode(childAssocRef); } - + + /** + * @see NodeServicePolicies.BeforeMoveNodePolicy#onMoveNode(ChildAssociationRef, NodeRef) + */ + protected void invokeBeforeMoveNode(ChildAssociationRef oldChildAssocRef, NodeRef newParentRef) + { + NodeRef childNodeRef = oldChildAssocRef.getChildRef(); + + if (ignorePolicy(childNodeRef)) + { + return; + } + + // get qnames to invoke against + Set qnames = getTypeAndAspectQNames(childNodeRef); + // execute policy for node type and aspects + NodeServicePolicies.BeforeMoveNodePolicy policy = beforeMoveNodeDelegate.get(childNodeRef, qnames); + policy.beforeMoveNode(oldChildAssocRef, newParentRef); + } + /** * @see NodeServicePolicies.OnMoveNodePolicy#onMoveNode(ChildAssociationRef, ChildAssociationRef) */ diff --git a/source/java/org/alfresco/repo/node/NodeServicePolicies.java b/source/java/org/alfresco/repo/node/NodeServicePolicies.java index 06bb39ea0a..d9891502c6 100644 --- a/source/java/org/alfresco/repo/node/NodeServicePolicies.java +++ b/source/java/org/alfresco/repo/node/NodeServicePolicies.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -88,7 +88,21 @@ public interface NodeServicePolicies */ public void onCreateNode(ChildAssociationRef childAssocRef); } - + + public interface BeforeMoveNodePolicy extends ClassPolicy + { + public static final QName QNAME = QName.createQName(NamespaceService.ALFRESCO_URI, "beforeMoveNode"); + /** + * Called before a node is moved. + * + * @param oldChildAssocRef the child association reference prior to the move + * @param newParentRef the new parent node reference + * + * @since 4.1 + */ + public void beforeMoveNode(ChildAssociationRef oldChildAssocRef, NodeRef newParentRef); + } + public interface OnMoveNodePolicy extends ClassPolicy { public static final QName QNAME = QName.createQName(NamespaceService.ALFRESCO_URI, "onMoveNode"); diff --git a/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java b/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java index 61155d9d0b..0a5e430544 100644 --- a/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java +++ b/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -2394,15 +2394,19 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl { // remove the deleted node from the list of new nodes untrackNewNodeRef(nodeToMoveRef); - + // track the deletion of this node - so we can prevent new associations to it. trackDeletedNodeRef(nodeToMoveRef); + // The Node changes NodeRefs, so this is really the deletion of the old node and creation + // of a node in a new store as far as the clients are concerned. + invokeBeforeDeleteNode(nodeToMoveRef); invokeBeforeCreateNode(newParentRef, assocTypeQName, assocQName, nodeToMoveTypeQName); } else { + invokeBeforeMoveNode(oldParentAssocRef, newParentRef); invokeBeforeDeleteChildAssociation(oldParentAssocRef); } @@ -2435,7 +2439,7 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl // Propagate timestamps propagateTimeStamps(oldParentAssocRef); propagateTimeStamps(newParentAssocRef); - + // The Node changes NodeRefs, so this is really the deletion of the old node and creation // of a node in a new store as far as the clients are concerned. invokeOnDeleteNode(oldParentAssocRef, nodeToMoveTypeQName, nodeToMoveAspectQNames, true); diff --git a/source/java/org/alfresco/repo/remoteconnector/RemoteConnectorServiceImpl.java b/source/java/org/alfresco/repo/remoteconnector/RemoteConnectorServiceImpl.java index 12d234daf0..c672f79e70 100644 --- a/source/java/org/alfresco/repo/remoteconnector/RemoteConnectorServiceImpl.java +++ b/source/java/org/alfresco/repo/remoteconnector/RemoteConnectorServiceImpl.java @@ -124,13 +124,22 @@ public class RemoteConnectorServiceImpl implements RemoteConnectorService String responseCharSet = httpRequest.getResponseCharSet(); String responseContentType = (responseContentTypeH != null ? responseContentTypeH.getValue() : null); + if(logger.isDebugEnabled()) + { + logger.debug("response url=" + request.getURL() + ", length =" + httpRequest.getResponseContentLength() + ", responceContentType " + responseContentType + ", statusText =" + statusText ); + } // Decide on how best to handle the response, based on the size // Ideally, we want to close the HttpClient resources immediately, but // that isn't possible for very large responses // If we can close immediately, it makes cleanup simpler and fool-proof - if (httpRequest.getResponseContentLength() > MAX_BUFFER_RESPONSE_SIZE) + if (httpRequest.getResponseContentLength() > MAX_BUFFER_RESPONSE_SIZE || httpRequest.getResponseContentLength() == -1 ) { + if(logger.isTraceEnabled()) + { + logger.trace("large response (or don't know length) url=" + request.getURL()); + } + // Need to wrap the InputStream in something that'll close InputStream wrappedStream = new HttpClientReleasingInputStream(httpRequest); httpRequest = null; @@ -141,6 +150,10 @@ public class RemoteConnectorServiceImpl implements RemoteConnectorService } else { + if(logger.isTraceEnabled()) + { + logger.debug("small response for url=" + request.getURL()); + } // Fairly small response, just keep the bytes and make life simple response = new RemoteConnectorResponseImpl(request, responseContentType, responseCharSet, status, responseHdrs, httpRequest.getResponseBody()); @@ -185,6 +198,10 @@ public class RemoteConnectorServiceImpl implements RemoteConnectorService { throw new RemoteConnectorServerException(status, statusText); } + if(status == Status.STATUS_PRECONDITION_FAILED) + { + throw new RemoteConnectorClientException(status, statusText, response); + } else { // Client request exceptions diff --git a/source/java/org/alfresco/repo/remotecredentials/PasswordCredentialsInfoImpl.java b/source/java/org/alfresco/repo/remotecredentials/PasswordCredentialsInfoImpl.java index efad24ff1d..fd0aa17b1e 100644 --- a/source/java/org/alfresco/repo/remotecredentials/PasswordCredentialsInfoImpl.java +++ b/source/java/org/alfresco/repo/remotecredentials/PasswordCredentialsInfoImpl.java @@ -55,4 +55,10 @@ public class PasswordCredentialsInfoImpl extends AbstractCredentialsImpl impleme { remotePassword = password; } + + public String toString() + { + return "Username & Password Credentials, Username="+getRemoteUsername()+" for System="+ + getRemoteSystemName() + " stored @ " + getNodeRef(); + } } diff --git a/source/java/org/alfresco/repo/remoteticket/AlfTicketRemoteAlfrescoTicketImpl.java b/source/java/org/alfresco/repo/remoteticket/AlfTicketRemoteAlfrescoTicketImpl.java index ff544798b5..f8bda24d65 100644 --- a/source/java/org/alfresco/repo/remoteticket/AlfTicketRemoteAlfrescoTicketImpl.java +++ b/source/java/org/alfresco/repo/remoteticket/AlfTicketRemoteAlfrescoTicketImpl.java @@ -67,4 +67,9 @@ public class AlfTicketRemoteAlfrescoTicketImpl extends AbstractRemoteAlfrescoTic { return new Pair(TICKET_USERNAME, ticket); } + + public String toString() + { + return "Remote Alfresco Ticket: " + ticket; + } } diff --git a/source/java/org/alfresco/repo/site/SiteServiceImpl.java b/source/java/org/alfresco/repo/site/SiteServiceImpl.java index 9822894834..239b9031ab 100644 --- a/source/java/org/alfresco/repo/site/SiteServiceImpl.java +++ b/source/java/org/alfresco/repo/site/SiteServiceImpl.java @@ -1116,7 +1116,13 @@ public class SiteServiceImpl extends AbstractLifecycleBean implements SiteServic if (visibilityValue == null) { // Examine each permission to see if this is a public site or not - Set permissions = this.permissionService.getAllSetPermissions(siteNodeRef); + Set permissions; + try { + permissions = this.permissionService.getAllSetPermissions(siteNodeRef); + } catch (AccessDeniedException ae){ + // We might not have permission to examine the permissions + return visibility; + } for (AccessPermission permission : permissions) { if (permission.getAuthority().equals(PermissionService.ALL_AUTHORITIES) == true && diff --git a/source/java/org/alfresco/repo/tenant/MultiTNodeServiceInterceptor.java b/source/java/org/alfresco/repo/tenant/MultiTNodeServiceInterceptor.java index 487663a0ec..508e8a2966 100644 --- a/source/java/org/alfresco/repo/tenant/MultiTNodeServiceInterceptor.java +++ b/source/java/org/alfresco/repo/tenant/MultiTNodeServiceInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -31,6 +31,7 @@ import org.alfresco.model.ContentModel; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.service.cmr.repository.AssociationRef; import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.Path; import org.alfresco.service.cmr.repository.StoreRef; @@ -134,7 +135,15 @@ public class MultiTNodeServiceInterceptor extends DelegatingIntroductionIntercep } // Make the call - Object ret = invocation.proceed(); + Object ret = null; + try + { + ret = invocation.proceed(); + } + catch (InvalidNodeRefException inre) + { + throw new InvalidNodeRefException(inre.getMessage(), tenantService.getBaseName(inre.getNodeRef())); + } if (methodName.equals("getProperty")) { @@ -246,7 +255,6 @@ public class MultiTNodeServiceInterceptor extends DelegatingIntroductionIntercep /** * Convert outbound collection to spoofed (ie. without tenant prefix) values. */ - @SuppressWarnings("unchecked") private Collection convertOutboundValues(Collection rawValues) { /* @@ -335,7 +343,6 @@ public class MultiTNodeServiceInterceptor extends DelegatingIntroductionIntercep /** * Convert inbound collection to non-spoofed (ie. with tenant prefix) values. */ - @SuppressWarnings("unchecked") private Collection convertInboundValues(Collection rawValues) { /* diff --git a/source/java/org/alfresco/repo/tenant/TenantRoutingFileContentStore.java b/source/java/org/alfresco/repo/tenant/TenantRoutingFileContentStore.java index c153856977..6cbde80899 100644 --- a/source/java/org/alfresco/repo/tenant/TenantRoutingFileContentStore.java +++ b/source/java/org/alfresco/repo/tenant/TenantRoutingFileContentStore.java @@ -20,19 +20,37 @@ package org.alfresco.repo.tenant; import java.io.File; +import org.alfresco.repo.content.ContentLimitProvider; +import org.alfresco.repo.content.ContentLimitProvider.NoLimitProvider; import org.alfresco.repo.content.ContentStore; import org.alfresco.repo.content.filestore.FileContentStore; import org.springframework.context.ApplicationContext; - - /** * MT-aware File Content Store */ public class TenantRoutingFileContentStore extends AbstractTenantRoutingContentStore { + private ContentLimitProvider contentLimitProvider = new NoLimitProvider(); + + /** + * Sets a new {@link ContentLimitProvider} which will provide a maximum filesize for content. + */ + public void setContentLimitProvider(ContentLimitProvider contentLimitProvider) + { + this.contentLimitProvider = contentLimitProvider; + } + protected ContentStore initContentStore(ApplicationContext ctx, String contentRoot) { - return new FileContentStore(ctx, new File(contentRoot)); + FileContentStore fileContentStore = new FileContentStore(ctx, new File(contentRoot)); + + // Set the content filesize limiter if there is one. + if (this.contentLimitProvider != null) + { + fileContentStore.setContentLimitProvider(contentLimitProvider); + } + + return fileContentStore; } } diff --git a/source/java/org/alfresco/service/cmr/lock/LockService.java b/source/java/org/alfresco/service/cmr/lock/LockService.java index c47652a4a7..1eb5035194 100644 --- a/source/java/org/alfresco/service/cmr/lock/LockService.java +++ b/source/java/org/alfresco/service/cmr/lock/LockService.java @@ -266,4 +266,16 @@ public interface LockService */ @Auditable(parameters = {"storeRef", "lockType"}) public List getLocks(StoreRef storeRef, LockType lockType); + + /** + * Allow the current transaction to update a node despite any locks that may be on it. + *

+ * Used for the system to be able to update locked nodes. + */ + public void suspendLocks(); + + /** + * After calling suspendLocks turn the locks back on. + */ + public void enableLocks(); } diff --git a/source/java/org/alfresco/service/cmr/lock/LockStatus.java b/source/java/org/alfresco/service/cmr/lock/LockStatus.java index d28bd698dd..6bf213b322 100644 --- a/source/java/org/alfresco/service/cmr/lock/LockStatus.java +++ b/source/java/org/alfresco/service/cmr/lock/LockStatus.java @@ -32,8 +32,20 @@ package org.alfresco.service.cmr.lock; */ public enum LockStatus { - NO_LOCK, // Indicates that there is no lock present - LOCKED, // Indicates that the node is locked - LOCK_OWNER, // Indicates that the node is locked and you have lock ownership rights - LOCK_EXPIRED // Indicates that the lock has expired and the node can be considered to be unlocked + /** + * Indicates that there is no lock present + */ + NO_LOCK, + /** + * Indicates that the node is locked + */ + LOCKED, + /** + * Indicates that the node is locked and you have lock ownership rights + */ + LOCK_OWNER, + /** + * Indicates that the lock has expired and the node can be considered to be unlocked + */ + LOCK_EXPIRED } \ No newline at end of file diff --git a/source/java/org/alfresco/service/descriptor/DescriptorService.java b/source/java/org/alfresco/service/descriptor/DescriptorService.java index 7d9ccb9252..c369a6e166 100644 --- a/source/java/org/alfresco/service/descriptor/DescriptorService.java +++ b/source/java/org/alfresco/service/descriptor/DescriptorService.java @@ -20,6 +20,7 @@ package org.alfresco.service.descriptor; import org.alfresco.service.NotAuditable; import org.alfresco.service.license.LicenseDescriptor; +import org.alfresco.service.license.LicenseService.LicenseChangeHandler; /** @@ -113,4 +114,10 @@ public interface DescriptorService * @return Returns a message telling the user what happened */ public String loadLicense(); + + /** + * Register a callback that gets called when a license changes. + */ + public void registerOnLicenseChange(LicenseChangeHandler callback); + } diff --git a/source/java/org/alfresco/service/license/LicenseDescriptor.java b/source/java/org/alfresco/service/license/LicenseDescriptor.java index 7a3921f5a1..7f6336316d 100644 --- a/source/java/org/alfresco/service/license/LicenseDescriptor.java +++ b/source/java/org/alfresco/service/license/LicenseDescriptor.java @@ -111,4 +111,11 @@ public interface LicenseDescriptor * @return the license mode. */ public LicenseMode getLicenseMode(); + + + /** + * Get the cloud sync key or null + * @return the cloud sync key + */ + public String getCloudSyncKey(); }