From d437d5105d3c910867c3ed40d4a3918178995b44 Mon Sep 17 00:00:00 2001 From: Dave Ward Date: Fri, 18 May 2012 17:00:53 +0000 Subject: [PATCH] Merged V4.0-BUG-FIX to HEAD 36311: BDE-69: filter long tests if minimal.testing property is defined 36314: Merged V4.0 to V4.0-BUG-FIX (RECORD ONLY) 36247: ALF-11027: temporarily remove import of maven.xml, since it makes ant calls fail from enterpriseprojects 36331: ALF-12447: Further changes required to fix lower case meta-inf folder name 36333: Revert ALF-12447. 36334: ALF-14115: Merged V3.4-BUG-FIX to V4.0-BUG-FIX 36318: ALF-12447: Fix case on META-INF folder for SDK 36332: ALF-12447: Further changes required to fix lower case meta-inf folder name 36337: ALF-14115: Merged V3.4-BUG-FIX to V4.0-BUG-FIX 36332: ALF-12447: Yet more meta-inf case changes needed. 36342: ALF-14120: fix only completed tasks returned 36343: ALF-13898: starting workflow from IMAP now using workflowDefs with engine name included, fallback to appending $jbpm when not present, to preserve backwards compatibility. 36345: Fix for ALF-12730 - Email Space Users fails if template is used 36346: Fix for ALF-9466 - We can search contents sorted by categories in Advanced search in Share, but saved search will not be shown in UI. 36364: Switch version to 4.0.3 36375: Merged BRANCHES/DEV/CLOUDSYNCLOCAL2 to BRANCHES/DEV/V4.0-BUG-FIX: 36366: Tweak to implementation to ensure that on-authentication-failed, the status is updated within a r/w transaction. 36374: Provide more specific exceptions from the Remote Connector Service for client and server errors 36376: Fix ALF-14121 - Alfresco fails to start if using "replicating-content-services-context.xml" 36393: Final part of ALF-13723 SOLR does not include the same query unit tests as lucene - CMIS typed query and ordering tests 36432: ALF-14133: Merged V3.4-BUG-FIX (3.4.10) to V4.0-BUG-FIX (4.0.3) << 4.0.x specific change: Changed transformer.complex.OOXML.Image into transformer.complex.Any.Image >> << allowing any transformer to be selected for the conversion to JPEG >> 36427: ALF-14131 Complex transformers fail if a lower level transformer fails even though there is another transformer that could do the transformation - Added a base spring bean for all complex transformers 36362: ALF-14131 Complex transformers fail if a lower level transformer fails even though there is another transformer that could do the transformation 36434: Test fix for ALF-13723 SOLR does not include the same query unit tests as lucene - CMIS test data change broke AFTS ID ordering 36503: Removed thousands of compiler warnings (CMIS query test code) 36518: Fix for ALF-13778 - Links on Share Repository search page show incorrect link name; do not work when root-node is defined. Fix now means that Share search correctly handles overridden Repository root node setting. Original work by Vasily Olhin. 36520: BDE-69: filter all repo tests if minimal.testing property is defined 36534: ALF-14116: Latest Surf libs (r1075) - ensure that i18n extensions can process browser sent short locales 36563: Merged V3.4-BUG-FIX to V4.0-BUG-FIX 36336: ALF-12447: Yet more meta-inf case changes needed. 36347: Fix for ALF-13920 - Error occurred when try to edit/delete category 36352: Fix for ALF-13123 - Invalid JSON format from Get Node Tags Webscript - strings not double-quoted. Also fixed POST webscript with same issue. 36399: ALL LANG: translation updates based on EN r36392 36421: Fix for Mac Lion versioning issue. ALF-12792 (Part 1 of 2) Enable the InfoPassthru and Level2Oplocks server capability flags, InfoPassthru is the flag that fixes the Mac Lion versioning error. Added support for filesystems that do not implement the NTFS streams interface in the CIFS transact rename processing, for the Alfresco repo filesystem. 36422: Fix for Mac Lion versioning issue. ALF-12792 (Part 2 of 2) Enable the InfoPassthru and Level2Oplocks server capability flags, InfoPassthru is the flag that fixes the Mac Lion versioning error. 36423: Add support for file size tracking in the file state. ALF-13616 (Part 1 of 2) 36424: Fix for Mac MS Word file save issue. ALF-13616 (Part 2 of 2) Added live file size tracking to file writing/folder searches so the correct file size is returned before the file is closed. 36444: Merged DEV to V3.4-BUG-FIX 36419: ALF-12666 Search against simple-search-additional-attributes doesn't work properly SearchContext.buildQuery(int) method was changed. 36446: Fix for ALF-13404 - Performance: 'Content I'm Editing' dashlet is slow to render when there is lots of data/sites - Effectively removed all PATH based queries using the pattern /companyhome/sites/*/container//* as they are a non-optimized case - Replaced the "all sites" doclist query using the above pattern with /companyhome/sites//* plus post query resultset processing based on documentLibrary container matching regex - Optimized favorite document query to remove need for a PATH - Optimized Content I'm Editing discussion PATH query to use /*/* instead of /*//* - Fixed issue where Content I'm Editing discussion results would not always show the root topics that a user has edited - Added some addition doclist.get.js query scriptlogger debugging output 36449: ALF-13404 - Fix for issue where favoriates for all sites would be shown in each site document library in the My Favorites filter. 36475: ALF-14131 Complex transformers fail if a lower level transformer fails even though there is another transformer that could do the transformation - Change base spring bean on example config file 36480: 36453: ALF-3881 : ldap sync deletion behaviour not flexible enough - synchronization.allowDeletions parameter introduced - default value is true (existing behaviour) - when false, no missing users or groups are deleted from the repository - instead they are cleared of their zones and missing groups are cleared of all their members - colliding users and groups from different zones are also 'moved' rather than recreated - unit test added 36491: Added CIFS transact2 NT passthru levels for set end of file/set allocation size. ALF-13616. Also updated FileInfoLevel with the latest list of NT passthru information levels. 36497: Fixed ALF-14163: JavaScript Behaviour broken: Node properties cannot be cast to java.io.Serializable - Fallout from ALF-12855 - Made class Serializable (like HashMap would have been) - Fixed line endings, too 36531: ALF-13769: Merged BELARUS/V3.4-BUG-FIX-2012_04_05 to V3.4-BUG-FIX (3.4.10) 35150: ALF-2645 : 3.2+ ldap sync debug information is too scarce - Improved LDAP logging. 36532: ALF-13769: BRANCHES/DEV/BELARUS/V3.4-BUG-FIX-2012_01_26 to V3.4-BUG-FIX (3.4.10) 36461: ALF-237: WCM: File conflicts cause file order not to be consistent - It is reasonable set values for checkboxes using the indexes from the list, which are not changed. So when we submit the window, the getSelectedNodes method is invoked and it takes selected nodes by checkbox values from "paths" list. 36535: Merged DEV to V3.4-BUG-FIX 36479: ALF-8918 : Cannot "edit offline" a web quick start publication A check in TaggableAspect.onUpdatePropertiesOnCommit() was extended to skip the update, if no tags were changed. 36555: Merged V3.4 to V3.4-BUG-FIX 36294: ALF-14039: Merged HEAD to V3.4 31732: ALF-10934: Prevent potential start/stop ping-pong of subsystems across a cluster - When a cluster boots up or receives a reinit message it shouldn't be sending out any start messages 36566: Merged V3.4-BUG-FIX to V4.0-BUG-FIX (RECORD ONLY) 36172: Merged BRANCHES/DEV/V4.0-BUG-FIX to BRANCHES/DEV/V3.4-BUG-FIX: 36169: ALF-8755: After renaming content / space by Contributor via WebDAV new items are created 36572: Merged V4.0 to V4.0-BUG-FIX 36388: ALF-14025: Updated Surf libs (1071). Fixes to checksum-disabled dependency handling 36392: ALF-14129 Failed to do upgrade from 3.4.8 to 4.0.2 << Committed change for Frederik Heremans >> - Moved actual activiti-tables creation to before the upgrade 36409: Fix for ALF-14124 Solr is not working - Errors occur during the startup 36466: Fix for ALF-12770 - Infinite loop popup alert in TinyMCE after XSS injection in Alfresco Explorer online edit. 36501: Merged DEV to V4.0 36496: ALF-14063 : CLONE - Internet Explorer hangs when using the object picker with a larger number of documents YUI 2.9.0 library was modified to use chunked unloading of listeners via a series of setTimeout() functions in event.js for IE 6,7,8. 36502: ALF-14105: Share Advanced search issue with the form values - Fix by David We 36538: ALF-13986: Updated web.xml and index.jsp redirect to ensure that SSO works with proper surf site-configuration customization 36539: Fix for ALF-14167 Filtering by Tags/Categories doen't findes any content in Repository/DocumentLibrary - fix default namespace back to "" -> "" and fix the specific SOLR tests that require otherwise. 36541: ALF-14082: Input stream leaks in thumbnail rendering webscripts 36560: Correctly size content length header after HTML stripping process (ALF-9365) 36574: Merged V4.0 to V4.0-BUG-FIX (RECORD ONLY) 36316: Merged V4.0-BUG-FIX to V4.0 (4.0.2) 36391: Merged V4.0-BUG-FIX to V4.0 36376: Fix ALF-14121 - Alfresco fails to start if using "replicating-content-services-context.xml" git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@36576 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- config/alfresco/content-services-context.xml | 29 +- .../alfresco/mt/mt-contentstore-context.xml | 2 +- .../default-synchronization-context.xml | 3 + .../default-synchronization.properties | 5 +- config/alfresco/swf-transform-context.xml | 10 +- .../bm-remote-manifest.mf | 0 source/{meta-inf => META-INF}/mmt-manifest.mf | 0 .../cifs/EnterpriseCifsAuthenticator.java | 2 +- .../repo/CacheLookupSearchContext.java | 5 +- .../filesys/repo/ContentDiskDriver.java | 8 +- .../filesys/repo/ContentNetworkFile.java | 6 +- .../repo/content/ContentServiceImpl.java | 11 +- .../replication/ReplicatingContentStore.java | 11 +- .../AbstractContentTransformerLimits.java | 8 +- .../transform/ComplexContentTransformer.java | 248 +++-- .../transform/ContentTransformerRegistry.java | 54 +- .../repo/domain/schema/SchemaBootstrap.java | 6 +- .../RemoteConnectorResponseImpl.java | 14 +- .../RemoteConnectorServiceImpl.java | 53 +- .../RemoteAlfrescoTicketServiceImpl.java | 11 +- .../ChainingUserRegistrySynchronizer.java | 337 +++++-- .../ChainingUserRegistrySynchronizerTest.java | 76 +- .../sync/UserRegistrySynchronizer.java | 4 +- .../security/sync/ldap/LDAPUserRegistry.java | 55 +- .../AbstractTenantRoutingContentStore.java | 18 +- .../repo/tenant/MultiTAdminServiceImpl.java | 61 +- .../tenant/TenantRoutingContentStore.java | 1 - .../RemoteConnectorClientException.java | 65 ++ .../RemoteConnectorResponse.java | 6 + .../RemoteConnectorServerException.java | 53 ++ .../RemoteConnectorService.java | 10 +- .../cmr/repository/ContentService.java | 41 +- .../TransformationOptionLimits.java | 36 +- .../TransformationOptionLimitsTest.java | 79 +- .../repository/TransformationOptionPair.java | 102 +- .../org/alfresco/util/ValueProtectingMap.java | 884 +++++++++--------- .../alfresco/util/ValueProtectingMapTest.java | 506 +++++----- source/test-resources/sync-test-context.xml | 34 + .../tenant/mt-contentstore-context.xml | 2 +- 39 files changed, 1855 insertions(+), 1001 deletions(-) rename source/{meta-inf => META-INF}/bm-remote-manifest.mf (100%) rename source/{meta-inf => META-INF}/mmt-manifest.mf (100%) create mode 100644 source/java/org/alfresco/service/cmr/remoteconnector/RemoteConnectorClientException.java create mode 100644 source/java/org/alfresco/service/cmr/remoteconnector/RemoteConnectorServerException.java diff --git a/config/alfresco/content-services-context.xml b/config/alfresco/content-services-context.xml index f9c73f1b76..744ceda35b 100644 --- a/config/alfresco/content-services-context.xml +++ b/config/alfresco/content-services-context.xml @@ -299,6 +299,17 @@ + + + + + + + + parent="baseComplexContentTransformer" > @@ -477,12 +488,14 @@ - + + parent="baseComplexContentTransformer"> - + @@ -495,7 +508,7 @@ + parent="baseComplexContentTransformer" > @@ -535,7 +548,7 @@ + parent="baseComplexContentTransformer" > @@ -565,7 +578,7 @@ class="org.alfresco.repo.content.transform.PoiContentTransformer" parent="baseContentTransformer" /> - + @@ -692,7 +705,7 @@ + parent="baseComplexContentTransformer" > diff --git a/config/alfresco/mt/mt-contentstore-context.xml b/config/alfresco/mt/mt-contentstore-context.xml index eee37fd75e..9362c1ca1d 100644 --- a/config/alfresco/mt/mt-contentstore-context.xml +++ b/config/alfresco/mt/mt-contentstore-context.xml @@ -8,7 +8,7 @@ - + diff --git a/config/alfresco/subsystems/Synchronization/default/default-synchronization-context.xml b/config/alfresco/subsystems/Synchronization/default/default-synchronization-context.xml index 9272d63016..951ec57d8e 100644 --- a/config/alfresco/subsystems/Synchronization/default/default-synchronization-context.xml +++ b/config/alfresco/subsystems/Synchronization/default/default-synchronization-context.xml @@ -74,6 +74,9 @@ ${synchronization.workerThreads} + + ${synchronization.allowDeletions} + diff --git a/config/alfresco/subsystems/Synchronization/default/default-synchronization.properties b/config/alfresco/subsystems/Synchronization/default/default-synchronization.properties index 36d8417e29..f73e71c637 100644 --- a/config/alfresco/subsystems/Synchronization/default/default-synchronization.properties +++ b/config/alfresco/subsystems/Synchronization/default/default-synchronization.properties @@ -27,4 +27,7 @@ synchronization.autoCreatePeopleOnLogin=true synchronization.loggingInterval=100 # The number of threads to use when doing a batch (scheduled or startup) sync -synchronization.workerThreads=2 \ No newline at end of file +synchronization.workerThreads=2 + +# Synchronization with deletions +synchronization.allowDeletions=true \ No newline at end of file diff --git a/config/alfresco/swf-transform-context.xml b/config/alfresco/swf-transform-context.xml index 30ea5141bd..2097bcb09a 100644 --- a/config/alfresco/swf-transform-context.xml +++ b/config/alfresco/swf-transform-context.xml @@ -28,7 +28,7 @@ + parent="baseComplexContentTransformer" > @@ -84,7 +84,7 @@ + parent="baseComplexContentTransformer" > @@ -100,7 +100,7 @@ + parent="baseComplexContentTransformer" > @@ -123,7 +123,7 @@ + parent="baseComplexContentTransformer" > @@ -147,7 +147,7 @@ + parent="baseComplexContentTransformer" > diff --git a/source/meta-inf/bm-remote-manifest.mf b/source/META-INF/bm-remote-manifest.mf similarity index 100% rename from source/meta-inf/bm-remote-manifest.mf rename to source/META-INF/bm-remote-manifest.mf diff --git a/source/meta-inf/mmt-manifest.mf b/source/META-INF/mmt-manifest.mf similarity index 100% rename from source/meta-inf/mmt-manifest.mf rename to source/META-INF/mmt-manifest.mf diff --git a/source/java/org/alfresco/filesys/auth/cifs/EnterpriseCifsAuthenticator.java b/source/java/org/alfresco/filesys/auth/cifs/EnterpriseCifsAuthenticator.java index d1adc9a7c8..0236119e67 100644 --- a/source/java/org/alfresco/filesys/auth/cifs/EnterpriseCifsAuthenticator.java +++ b/source/java/org/alfresco/filesys/auth/cifs/EnterpriseCifsAuthenticator.java @@ -609,7 +609,7 @@ public class EnterpriseCifsAuthenticator extends CifsAuthenticatorBase implement { return Capability.Unicode + Capability.RemoteAPIs + Capability.NTSMBs + Capability.NTFind + Capability.NTStatus + Capability.LargeFiles + Capability.LargeRead + Capability.LargeWrite + - Capability.ExtendedSecurity; + Capability.ExtendedSecurity + Capability.InfoPassthru + Capability.Level2Oplocks; } /** diff --git a/source/java/org/alfresco/filesys/repo/CacheLookupSearchContext.java b/source/java/org/alfresco/filesys/repo/CacheLookupSearchContext.java index af0bbfed6d..24b89c5db6 100644 --- a/source/java/org/alfresco/filesys/repo/CacheLookupSearchContext.java +++ b/source/java/org/alfresco/filesys/repo/CacheLookupSearchContext.java @@ -104,8 +104,11 @@ public class CacheLookupSearchContext extends DotDotContentSearchContext { if ( fstate.hasModifyDateTime()) info.setModifyDateTime( fstate.getModifyDateTime()); - // File allocation size + // File used/allocation size + if ( fstate.hasFileSize()) + info.setFileSize( fstate.getFileSize()); + if ( fstate.hasAllocationSize() && fstate.getAllocationSize() > info.getSize()) info.setAllocationSize( fstate.getAllocationSize()); diff --git a/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java b/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java index 03400ef54b..965b0ba7f7 100644 --- a/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java +++ b/source/java/org/alfresco/filesys/repo/ContentDiskDriver.java @@ -1353,8 +1353,12 @@ public class ContentDiskDriver extends AlfrescoTxDiskDriver implements DiskInter } } } - else - searchCtx = new ContentSearchContext(cifsHelper, results, searchFileSpec, pseudoList, paths[0]); + else { + if ( ctx.hasStateCache()) + searchCtx = new CacheLookupSearchContext(cifsHelper, results, searchFileSpec, pseudoList, paths[0], ctx.getStateCache()); + else + searchCtx = new ContentSearchContext(cifsHelper, results, searchFileSpec, pseudoList, paths[0]); + } // Debug diff --git a/source/java/org/alfresco/filesys/repo/ContentNetworkFile.java b/source/java/org/alfresco/filesys/repo/ContentNetworkFile.java index 602c559700..8a1bf5e0dc 100644 --- a/source/java/org/alfresco/filesys/repo/ContentNetworkFile.java +++ b/source/java/org/alfresco/filesys/repo/ContentNetworkFile.java @@ -661,10 +661,12 @@ public class ContentNetworkFile extends NodeRefNetworkFile setFileSize(channel.size()); - // Update the modification date/time + // Update the modification date/time and live file size - if ( getFileState() != null) + if ( getFileState() != null) { getFileState().updateModifyDateTime(); + getFileState().setFileSize( getFileSize()); + } // DEBUG diff --git a/source/java/org/alfresco/repo/content/ContentServiceImpl.java b/source/java/org/alfresco/repo/content/ContentServiceImpl.java index 2d8763e59d..b537e192a8 100644 --- a/source/java/org/alfresco/repo/content/ContentServiceImpl.java +++ b/source/java/org/alfresco/repo/content/ContentServiceImpl.java @@ -738,6 +738,15 @@ public class ContentServiceImpl implements ContentService, ApplicationContextAwa * @see org.alfresco.service.cmr.repository.ContentService#getTransformer(String, java.lang.String, long, java.lang.String, org.alfresco.service.cmr.repository.TransformationOptions) */ public ContentTransformer getTransformer(String sourceUrl, String sourceMimetype, long sourceSize, String targetMimetype, TransformationOptions options) + { + List transformers = getTransformers(sourceUrl, sourceMimetype, sourceSize, targetMimetype, options); + return (transformers == null) ? null : transformers.get(0); + } + + /** + * @see org.alfresco.service.cmr.repository.ContentService#getTransformers(String, java.lang.String, long, java.lang.String, org.alfresco.service.cmr.repository.TransformationOptions) + */ + public List getTransformers(String sourceUrl, String sourceMimetype, long sourceSize, String targetMimetype, TransformationOptions options) { try { @@ -745,7 +754,7 @@ public class ContentServiceImpl implements ContentService, ApplicationContextAwa transformerDebug.pushAvailable(sourceUrl, sourceMimetype, targetMimetype, options); List transformers = getActiveTransformers(sourceMimetype, sourceSize, targetMimetype, options); transformerDebug.availableTransformers(transformers, sourceSize, "ContentService.getTransformer(...)"); - return (transformers.isEmpty()) ? null : transformers.get(0); + return transformers.isEmpty() ? null : transformers; } finally { diff --git a/source/java/org/alfresco/repo/content/replication/ReplicatingContentStore.java b/source/java/org/alfresco/repo/content/replication/ReplicatingContentStore.java index dde3eae186..7f3d7a1e8d 100644 --- a/source/java/org/alfresco/repo/content/replication/ReplicatingContentStore.java +++ b/source/java/org/alfresco/repo/content/replication/ReplicatingContentStore.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 * @@ -213,6 +213,15 @@ public class ReplicatingContentStore extends AbstractContentStore { return primaryStore.isContentUrlSupported(contentUrl); } + + /** + * @return Return the primary store root location + */ + @Override + public String getRootLocation() + { + return primaryStore.getRootLocation(); + } /** * Forwards the call directly to the first store in the list of stores. diff --git a/source/java/org/alfresco/repo/content/transform/AbstractContentTransformerLimits.java b/source/java/org/alfresco/repo/content/transform/AbstractContentTransformerLimits.java index ea441098cd..f9531c8651 100644 --- a/source/java/org/alfresco/repo/content/transform/AbstractContentTransformerLimits.java +++ b/source/java/org/alfresco/repo/content/transform/AbstractContentTransformerLimits.java @@ -62,7 +62,7 @@ public abstract class AbstractContentTransformerLimits extends ContentTransforme * Indicates if 'page' limits are supported. * @return false by default. */ - protected boolean isPageLimitSupported() + protected boolean isPageLimitSupported(String sourceMimetype, String targetMimetype, TransformationOptions options) { return pageLimitsSupported; } @@ -98,6 +98,10 @@ public abstract class AbstractContentTransformerLimits extends ContentTransforme @Override public boolean isTransformable(String sourceMimetype, long sourceSize, String targetMimetype, TransformationOptions options) { + // To make TransformerDebug output clearer, check the mimetypes and then the sizes. + // If not done, 'unavailable' transformers due to size might be reported even + // though they cannot transform the source to the target mimetype. + return isTransformableMimetype(sourceMimetype, targetMimetype, options) && isTransformableSize(sourceMimetype, sourceSize, targetMimetype, options); @@ -152,7 +156,7 @@ public abstract class AbstractContentTransformerLimits extends ContentTransforme // of icons. Note the readLimitKBytes value is not checked as the combined limits // only have the max or limit KBytes value set (the smaller value is returned). TransformationOptionLimits limits = getLimits(sourceMimetype, targetMimetype, options); - if (!isPageLimitSupported() || limits.getPageLimit() <= 0) + if (!isPageLimitSupported(sourceMimetype, targetMimetype, options) || limits.getPageLimit() <= 0) { maxSourceSizeKBytes = limits.getMaxSourceSizeKBytes(); } diff --git a/source/java/org/alfresco/repo/content/transform/ComplexContentTransformer.java b/source/java/org/alfresco/repo/content/transform/ComplexContentTransformer.java index aedde22ce3..6ffc4e7273 100644 --- a/source/java/org/alfresco/repo/content/transform/ComplexContentTransformer.java +++ b/source/java/org/alfresco/repo/content/transform/ComplexContentTransformer.java @@ -22,7 +22,10 @@ import java.beans.PropertyDescriptor; import java.io.File; import java.io.Serializable; import java.lang.reflect.InvocationTargetException; +import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Collections; +import java.util.Deque; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -32,6 +35,7 @@ import javax.faces.el.MethodNotFoundException; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.repo.content.filestore.FileContentWriter; import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.TransformationOptionLimits; @@ -57,20 +61,37 @@ public class ComplexContentTransformer extends AbstractContentTransformer2 imple */ private static Log logger = LogFactory.getLog(ComplexContentTransformer.class); + /** + * Complex transformers contain lower level transformers. In order to find dynamic + * (defined as null) child transformers to use, they recursively check available + * transformers. It makes no sense to have a transformer that is its own child. + */ + static final ThreadLocal> parentTransformers = new ThreadLocal>() { + @Override + protected Deque initialValue() { + return new ArrayDeque(); + } + }; + private List transformers; private List intermediateMimetypes; private Map transformationOptionOverrides; - + private ContentService contentService; + public ComplexContentTransformer() { } /** - * The list of transformers to use. + * The list of transformers to use. If any element is null + * all possible transformers will be considered. If any element + * is null, the contentService property must be set. *

* If a single transformer is supplied, then it will still be used. * * @param transformers list of at least one transformer + * + * @see #setContentService(ContentService) */ public void setTransformers(List transformers) { @@ -107,6 +128,16 @@ public class ComplexContentTransformer extends AbstractContentTransformer2 imple this.transformationOptionOverrides = transformationOptionOverrides; } + /** + * Sets the ContentService. Only required if {@code null} transformers + * are provided to {@link #setTransformers(List). + * @param contentService + */ + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + /** * Ensures that required properties have been set */ @@ -125,25 +156,35 @@ public class ComplexContentTransformer extends AbstractContentTransformer2 imple { throw new AlfrescoRuntimeException("'mimetypeService' is a required property"); } + for (ContentTransformer transformer: transformers) + { + if (transformer == null) + { + if (contentService == null) + { + throw new AlfrescoRuntimeException("'contentService' is a required property if " + + "there are any null (dynamic) transformers"); + } + break; + } + } } - /** - * Overrides this method to avoid calling - * {@link #isTransformableMimetype(String, String, TransformationOptions)} - * twice on each transformer in the list, as - * {@link #isTransformableSize(String, long, String, TransformationOptions)} - * in this class must check the mimetype too. - */ @Override public boolean isTransformable(String sourceMimetype, long sourceSize, String targetMimetype, TransformationOptions options) { + // Don't allow transformer to be its own child. + if (parentTransformers.get().contains(this)) + { + return false; + } + overrideTransformationOptions(options); - // To make TransformerDebug output clearer, check the mimetypes and then the sizes. - // If not done, 'unavailable' transformers due to size might be reported even - // though they cannot transform the source to the target mimetype. - + // Can use super isTransformableSize as it indirectly calls getLimits in this class + // which combines the limits from the first transformer. Other transformer in the chain + // are no checked as sizes are unknown. return isTransformableMimetype(sourceMimetype, targetMimetype, options) && isTransformableSize(sourceMimetype, sourceSize, targetMimetype, options); @@ -200,73 +241,42 @@ public class ComplexContentTransformer extends AbstractContentTransformer2 imple @Override public boolean isTransformableMimetype(String sourceMimetype, String targetMimetype, TransformationOptions options) - { - return isTransformableMimetypeAndSize(sourceMimetype, -1, targetMimetype, options); - } - - @Override - public boolean isTransformableSize(String sourceMimetype, long sourceSize, String targetMimetype, TransformationOptions options) - { - return (sourceSize < 0) || - super.isTransformableSize(sourceMimetype, sourceSize, targetMimetype, options) && - isTransformableMimetypeAndSize(sourceMimetype, sourceSize, targetMimetype, options); - } - - private boolean isTransformableMimetypeAndSize(String sourceMimetype, long sourceSize, - String targetMimetype, TransformationOptions options) { boolean result = true; String currentSourceMimetype = sourceMimetype; - Iterator transformerIterator = transformers.iterator(); Iterator intermediateMimetypeIterator = intermediateMimetypes.iterator(); while (transformerIterator.hasNext()) { ContentTransformer transformer = transformerIterator.next(); - // determine the target mimetype. This is the final target if we are on the last transformation - String currentTargetMimetype = null; - if (!transformerIterator.hasNext()) + + // determine the target mimetype. This is the final target if we are on the last transformation + String currentTargetMimetype = transformerIterator.hasNext() ? intermediateMimetypeIterator.next() : targetMimetype; + if (transformer == null) { - currentTargetMimetype = targetMimetype; - } - else - { - // use an intermediate transformation mimetype - currentTargetMimetype = intermediateMimetypeIterator.next(); - } - - if (sourceSize < 0) - { - // check we can transform the current stage's mimetypes - if (transformer.isTransformableMimetype(currentSourceMimetype, currentTargetMimetype, options) == false) - { - result = false; - break; - } - } - else - { - // check we can transform the current stage's sizes try { - transformerDebug.pushIsTransformableSize(this); - // (using -1 if not the first stage as we can't know the size) - if (transformer.isTransformableSize(currentSourceMimetype, sourceSize, currentTargetMimetype, options) == false) + parentTransformers.get().push(this); + @SuppressWarnings("deprecation") + List firstTansformers = contentService.getActiveTransformers( + currentSourceMimetype, -1, currentTargetMimetype, options); + if (firstTansformers.isEmpty()) { result = false; break; } - - // As the size is unknown for the next stages stop. - // In future we might guess sizes such as excl to pdf - // is about 110% of the original size, in which case - // we would continue. - break; - // sourceSize += sourceSize * 10 / 100; } finally { - transformerDebug.popIsTransformableSize(); + parentTransformers.get().pop(); + } + } + else + { + if (transformer.isTransformableMimetype(currentSourceMimetype, currentTargetMimetype, options) == false) + { + result = false; + break; } } @@ -279,30 +289,111 @@ public class ComplexContentTransformer extends AbstractContentTransformer2 imple /** * Indicates if 'page' limits are supported by the first transformer in the chain. + * If the first transformer is dynamic, all possible first transformers must support it. * @return true if the first transformer supports them. */ - protected boolean isPageLimitSupported() + @Override + protected boolean isPageLimitSupported(String sourceMimetype, String targetMimetype, + TransformationOptions options) { - ContentTransformer firstTransformer = transformers.iterator().next(); - return (firstTransformer instanceof AbstractContentTransformerLimits) - ? ((AbstractContentTransformerLimits)firstTransformer).isPageLimitSupported() + boolean pageLimitSupported; + ContentTransformer firstTransformer = transformers.get(0); + String firstTargetMimetype = intermediateMimetypes.get(0); + if (firstTransformer == null) + { + try + { + parentTransformers.get().push(this); + @SuppressWarnings("deprecation") + List firstTansformers = contentService.getActiveTransformers( + sourceMimetype, -1, firstTargetMimetype, options); + pageLimitSupported = !firstTansformers.isEmpty(); + if (pageLimitSupported) + { + for (ContentTransformer transformer: firstTansformers) + { + if (!isPageLimitSupported(transformer, sourceMimetype, targetMimetype, options)) + { + pageLimitSupported = false; + break; + } + } + } + } + finally + { + parentTransformers.get().pop(); + } + } + else + { + pageLimitSupported = isPageLimitSupported(firstTransformer, sourceMimetype, targetMimetype, options); + } + return pageLimitSupported; + } + + private boolean isPageLimitSupported(ContentTransformer transformer, String sourceMimetype, + String targetMimetype, TransformationOptions options) + { + return (transformer instanceof AbstractContentTransformerLimits) + ? ((AbstractContentTransformerLimits)transformer).isPageLimitSupported(sourceMimetype, targetMimetype, options) : false; } /** * Returns the limits from this transformer combined with those of the first transformer in the chain. + * If the first transformer is dynamic, the lowest common denominator between all possible first transformers + * are combined. */ protected TransformationOptionLimits getLimits(String sourceMimetype, String targetMimetype, TransformationOptions options) { + TransformationOptionLimits firstTransformerLimits = null; TransformationOptionLimits limits = super.getLimits(sourceMimetype, targetMimetype, options); ContentTransformer firstTransformer = transformers.get(0); - if (firstTransformer instanceof AbstractContentTransformerLimits) + String firstTargetMimetype = intermediateMimetypes.get(0); + if (firstTransformer == null) { - String firstTargetMimetype = intermediateMimetypes.get(0); - limits = limits.combine(((AbstractContentTransformerLimits) firstTransformer). - getLimits(sourceMimetype, firstTargetMimetype, options)); + try + { + parentTransformers.get().push(this); + @SuppressWarnings("deprecation") + List firstTansformers = contentService.getActiveTransformers( + sourceMimetype, -1, firstTargetMimetype, options); + if (!firstTansformers.isEmpty()) + { + for (ContentTransformer transformer: firstTansformers) + { + if (transformer instanceof AbstractContentTransformerLimits) + { + TransformationOptionLimits transformerLimits = ((AbstractContentTransformerLimits)transformer). + getLimits(sourceMimetype, firstTargetMimetype, options); + firstTransformerLimits = (firstTransformerLimits == null) + ? transformerLimits + : firstTransformerLimits.combineUpper(transformerLimits); + } + } + } + } + finally + { + parentTransformers.get().pop(); + } } + else + { + if (firstTransformer instanceof AbstractContentTransformerLimits) + { + firstTransformerLimits = ((AbstractContentTransformerLimits)firstTransformer). + getLimits(sourceMimetype, firstTargetMimetype, options); + } + } + + if (firstTransformerLimits != null) + { + limits = limits.combine(firstTransformerLimits); + } + return limits; } @@ -345,7 +436,22 @@ public class ComplexContentTransformer extends AbstractContentTransformer2 imple } // transform - transformer.transform(currentReader, currentWriter, options); + if (transformer == null) + { + try + { + parentTransformers.get().push(this); + contentService.transform(currentReader, currentWriter, options); + } + finally + { + parentTransformers.get().pop(); + } + } + else + { + transformer.transform(currentReader, currentWriter, options); + } // Must clear the sourceNodeRef after the first transformation to avoid later // transformers thinking the intermediate file is the original node. However as diff --git a/source/java/org/alfresco/repo/content/transform/ContentTransformerRegistry.java b/source/java/org/alfresco/repo/content/transform/ContentTransformerRegistry.java index df6c47acb7..205b7f13fb 100644 --- a/source/java/org/alfresco/repo/content/transform/ContentTransformerRegistry.java +++ b/source/java/org/alfresco/repo/content/transform/ContentTransformerRegistry.java @@ -109,20 +109,11 @@ public class ContentTransformerRegistry { // Get the list of transformers List transformers = findTransformers(sourceMimetype, sourceSize, targetMimetype, options); - final Map activeTransformers = new HashMap(); // identify the performance of all the transformers for (ContentTransformer transformer : transformers) { - // Transformability can be dynamic, i.e. it may have become unusable - // Don't know why we do this test as it has already been done by findTransformers(...) - if (transformer.isTransformable(sourceMimetype, sourceSize, targetMimetype, options) == false) - { - // It is unreliable now. - continue; - } - long transformationTime = transformer.getTransformationTime(); activeTransformers.put(transformer, transformationTime); } @@ -151,34 +142,6 @@ public class ContentTransformerRegistry */ private List findTransformers(String sourceMimetype, long sourceSize, String targetMimetype, TransformationOptions options) { - // search for a simple transformer that can do the job - List transformers = findDirectTransformers(sourceMimetype, sourceSize, targetMimetype, options); - // get the complex transformers that can do the job - List complexTransformers = findComplexTransformer(sourceMimetype, targetMimetype, options); - transformers.addAll(complexTransformers); - // done - if (logger.isDebugEnabled()) - { - logger.debug("Searched for transformer: \n" + - " source mimetype: " + sourceMimetype + "\n" + - " target mimetype: " + targetMimetype + "\n" + - " transformers: " + transformers); - } - return transformers; - } - - /** - * Loops through the content transformers and picks the ones with the highest reliabilities. - *

- * Where there are several transformers that are equally reliable, they are all returned. - * - * @return Returns the most reliable transformers for the translation - empty list if there - * are none. - */ - private List findDirectTransformers(String sourceMimetype, long sourceSize, String targetMimetype, TransformationOptions options) - { - //double maxReliability = 0.0; - List transformers = new ArrayList(2); boolean foundExplicit = false; @@ -206,19 +169,16 @@ public class ContentTransformerRegistry } } // done + if (logger.isDebugEnabled()) + { + logger.debug("Searched for transformer: \n" + + " source mimetype: " + sourceMimetype + "\n" + + " target mimetype: " + targetMimetype + "\n" + + " transformers: " + transformers); + } return transformers; } - /** - * Uses a list of known mimetypes to build transformations from several direct transformations. - */ - private List findComplexTransformer(String sourceMimetype, String targetMimetype, TransformationOptions options) - { - // get a complete list of mimetypes - // TODO: Build complex transformers by searching for transformations by mimetype - return Collections.emptyList(); - } - /** * Recursive method to build up a list of content transformers */ diff --git a/source/java/org/alfresco/repo/domain/schema/SchemaBootstrap.java b/source/java/org/alfresco/repo/domain/schema/SchemaBootstrap.java index b6d9267c3c..f114ded2d0 100644 --- a/source/java/org/alfresco/repo/domain/schema/SchemaBootstrap.java +++ b/source/java/org/alfresco/repo/domain/schema/SchemaBootstrap.java @@ -783,6 +783,9 @@ public class SchemaBootstrap extends AbstractLifecycleBean final Dialect dialect = Dialect.getDialect(cfg.getProperties()); String dialectStr = dialect.getClass().getSimpleName(); + // Initialise Activiti DB, using an unclosable connection. + initialiseActivitiDBSchema(new UnclosableConnection(connection)); + if (create) { // execute pre-create scripts (not patches) @@ -865,9 +868,6 @@ public class SchemaBootstrap extends AbstractLifecycleBean checkSchemaPatchScripts(cfg, connection, postUpdateScriptPatches, true); } - // Initialise Activiti DB, using an unclosable connection - initialiseActivitiDBSchema(new UnclosableConnection(connection)); - return create; } diff --git a/source/java/org/alfresco/repo/remoteconnector/RemoteConnectorResponseImpl.java b/source/java/org/alfresco/repo/remoteconnector/RemoteConnectorResponseImpl.java index b926974784..6b0726fcd3 100644 --- a/source/java/org/alfresco/repo/remoteconnector/RemoteConnectorResponseImpl.java +++ b/source/java/org/alfresco/repo/remoteconnector/RemoteConnectorResponseImpl.java @@ -42,6 +42,7 @@ public class RemoteConnectorResponseImpl implements RemoteConnectorResponse private String contentType; private String charset; + private int status; private Header[] headers; private InputStream bodyStream; @@ -53,21 +54,28 @@ public class RemoteConnectorResponseImpl implements RemoteConnectorResponse * InputStream shouldn't be used as cleanup is needed */ public RemoteConnectorResponseImpl(RemoteConnectorRequest request, String contentType, - String charset, Header[] headers, InputStream response) + String charset, int status, Header[] headers, InputStream response) { this.request = request; this.contentType = contentType; this.charset = charset; this.headers = headers; + this.status = status; this.bodyStream = response; this.bodyBytes = null; } public RemoteConnectorResponseImpl(RemoteConnectorRequest request, String contentType, - String charset, Header[] headers, byte[] response) + String charset, int status, Header[] headers, byte[] response) { - this(request, contentType, charset, headers, new ByteArrayInputStream(response)); + this(request, contentType, charset, status, headers, new ByteArrayInputStream(response)); this.bodyBytes = response; } + + @Override + public int getStatus() + { + return status; + } @Override public String getCharset() diff --git a/source/java/org/alfresco/repo/remoteconnector/RemoteConnectorServiceImpl.java b/source/java/org/alfresco/repo/remoteconnector/RemoteConnectorServiceImpl.java index 9c50dd6029..12d234daf0 100644 --- a/source/java/org/alfresco/repo/remoteconnector/RemoteConnectorServiceImpl.java +++ b/source/java/org/alfresco/repo/remoteconnector/RemoteConnectorServiceImpl.java @@ -24,8 +24,10 @@ import java.io.InputStream; import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.security.authentication.AuthenticationException; +import org.alfresco.service.cmr.remoteconnector.RemoteConnectorClientException; import org.alfresco.service.cmr.remoteconnector.RemoteConnectorRequest; import org.alfresco.service.cmr.remoteconnector.RemoteConnectorResponse; +import org.alfresco.service.cmr.remoteconnector.RemoteConnectorServerException; import org.alfresco.service.cmr.remoteconnector.RemoteConnectorService; import org.alfresco.util.HttpClientHelper; import org.apache.commons.httpclient.Header; @@ -79,7 +81,8 @@ public class RemoteConnectorServiceImpl implements RemoteConnectorService /** * Executes the specified request, and return the response */ - public RemoteConnectorResponse executeRequest(RemoteConnectorRequest request) throws IOException, AuthenticationException + public RemoteConnectorResponse executeRequest(RemoteConnectorRequest request) throws IOException, AuthenticationException, + RemoteConnectorClientException, RemoteConnectorServerException { RemoteConnectorRequestImpl reqImpl = (RemoteConnectorRequestImpl)request; HttpMethodBase httpRequest = reqImpl.getMethodInstance(); @@ -134,13 +137,13 @@ public class RemoteConnectorServiceImpl implements RemoteConnectorService // Now build the response response = new RemoteConnectorResponseImpl(request, responseContentType, responseCharSet, - responseHdrs, wrappedStream); + status, responseHdrs, wrappedStream); } else { // Fairly small response, just keep the bytes and make life simple response = new RemoteConnectorResponseImpl(request, responseContentType, responseCharSet, - responseHdrs, httpRequest.getResponseBody()); + status, responseHdrs, httpRequest.getResponseBody()); // Now we have the bytes, we can close the HttpClient resources httpRequest.releaseConnection(); @@ -164,26 +167,42 @@ public class RemoteConnectorServiceImpl implements RemoteConnectorService logger.debug("Response was " + status + " " + statusText); // Decide if we should throw an exception - if (status == Status.STATUS_FORBIDDEN) + if (status >= 300) { // Tidy if needed if (httpRequest != null) httpRequest.releaseConnection(); - // Then report the error - throw new AuthenticationException(statusText); + + // Specific exceptions + if (status == Status.STATUS_FORBIDDEN || + status == Status.STATUS_UNAUTHORIZED) + { + throw new AuthenticationException(statusText); + } + + // Server side exceptions + if (status >= 500 && status <= 599) + { + throw new RemoteConnectorServerException(status, statusText); + } + else + { + // Client request exceptions + if (httpRequest != null) + { + // Response wasn't too big and is available, supply it + throw new RemoteConnectorClientException(status, statusText, response); + } + else + { + // Response was too large, report without it + throw new RemoteConnectorClientException(status, statusText, null); + } + } } - if (status == Status.STATUS_INTERNAL_SERVER_ERROR) - { - // Tidy if needed - if (httpRequest != null) - httpRequest.releaseConnection(); - // Then report the error - throw new IOException(statusText); - } - // TODO Handle the rest of the different status codes - - // Return our created response + // If we get here, then the request/response was all fine + // So, return our created response return response; } diff --git a/source/java/org/alfresco/repo/remoteticket/RemoteAlfrescoTicketServiceImpl.java b/source/java/org/alfresco/repo/remoteticket/RemoteAlfrescoTicketServiceImpl.java index 5151529807..7b6a8c0a73 100644 --- a/source/java/org/alfresco/repo/remoteticket/RemoteAlfrescoTicketServiceImpl.java +++ b/source/java/org/alfresco/repo/remoteticket/RemoteAlfrescoTicketServiceImpl.java @@ -404,7 +404,16 @@ public class RemoteAlfrescoTicketServiceImpl implements RemoteAlfrescoTicketServ // If the credentials indicate the previous attempt failed, record as now working if (! credentials.getLastAuthenticationSucceeded()) { - remoteCredentialsService.updateCredentialsAuthenticationSucceeded(true, credentials); + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + public Void execute() + { + remoteCredentialsService.updateCredentialsAuthenticationSucceeded(true, credentials); + return null; + } + }, false, true + ); } // Wrap and return diff --git a/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizer.java b/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizer.java index fe9e80bf51..1df3de8633 100644 --- a/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizer.java +++ b/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2011 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -18,7 +18,10 @@ */ package org.alfresco.repo.security.sync; +import java.io.IOException; import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; import java.text.DateFormat; import java.util.Collection; import java.util.Collections; @@ -38,6 +41,17 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import javax.management.AttributeNotFoundException; +import javax.management.InstanceNotFoundException; +import javax.management.IntrospectionException; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanException; +import javax.management.MBeanInfo; +import javax.management.MBeanServerConnection; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; +import javax.management.ReflectionException; + import org.alfresco.model.ContentModel; import org.alfresco.repo.batch.BatchProcessor; import org.alfresco.repo.batch.BatchProcessor.BatchProcessWorker; @@ -161,6 +175,11 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl /** The number of worker threads. */ private int workerThreads = 2; + + private MBeanServerConnection mbeanServer; + + /** Allow a full sync to perform deletions? */ + private boolean allowDeletions = true; /** * Sets the application context manager. @@ -315,13 +334,51 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl { this.workerThreads = workerThreads; } - + + /** + * Fullsync is run with deletions. By default is set to true. + * + * @param allowDeletions + */ + public void setAllowDeletions(boolean allowDeletions) + { + this.allowDeletions = allowDeletions; + } + /* * (non-Javadoc) * @see org.alfresco.repo.security.sync.UserRegistrySynchronizer#synchronize(boolean, boolean, boolean) */ - public void synchronize(boolean forceUpdate, boolean allowDeletions, final boolean splitTxns) + public void synchronize(boolean forceUpdate, boolean isFullSync, final boolean splitTxns) { + if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled()) + { + + if (forceUpdate) + { + ChainingUserRegistrySynchronizer.logger.debug("Running a full sync."); + } + else + { + ChainingUserRegistrySynchronizer.logger.debug("Running a differential sync."); + } + if (allowDeletions) + { + ChainingUserRegistrySynchronizer.logger.debug("deletions are allowed"); + } + else + { + ChainingUserRegistrySynchronizer.logger.debug("deletions are not allowed"); + } + // Don't proceed with the sync if the repository is read only + if (this.transactionService.isReadOnly()) + { + ChainingUserRegistrySynchronizer.logger + .warn("Unable to proceed with user registry synchronization. Repository is read only."); + return; + } + } + // Don't proceed with the sync if the repository is read only if (this.transactionService.isReadOnly()) { @@ -414,17 +471,112 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl UserRegistry plugin = (UserRegistry) context.getBean(this.sourceBeanName); if (!(plugin instanceof ActivateableBean) || ((ActivateableBean) plugin).isActive()) { + if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled()) + { + mbeanServer = (MBeanServerConnection) getApplicationContext().getBean("alfrescoMBeanServer"); + try + { + StringBuilder nameBuff = new StringBuilder(200).append("Alfresco:Type=Configuration,Category=Authentication,id1=managed,id2=").append( + URLDecoder.decode(id, "UTF-8")); + ObjectName name = new ObjectName(nameBuff.toString()); + if (mbeanServer != null && mbeanServer.isRegistered(name)) + { + MBeanInfo info = mbeanServer.getMBeanInfo(name); + MBeanAttributeInfo[] attributes = info.getAttributes(); + ChainingUserRegistrySynchronizer.logger.debug(id + " attributes:"); + for (MBeanAttributeInfo attribute : attributes) + { + Object value = mbeanServer.getAttribute(name, attribute.getName()); + ChainingUserRegistrySynchronizer.logger.debug(attribute.getName() + " = " + value); + } + } + } + catch(UnsupportedEncodingException e) + { + if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) + { + ChainingUserRegistrySynchronizer.logger + .warn("Exception during logging", e); + } + } + catch (MalformedObjectNameException e) + { + if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) + { + ChainingUserRegistrySynchronizer.logger + .warn("Exception during logging", e); + } + } + catch (InstanceNotFoundException e) + { + if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) + { + ChainingUserRegistrySynchronizer.logger + .warn("Exception during logging", e); + } + } + catch (IntrospectionException e) + { + if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) + { + ChainingUserRegistrySynchronizer.logger + .warn("Exception during logging", e); + } + } + catch (AttributeNotFoundException e) + { + if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) + { + ChainingUserRegistrySynchronizer.logger + .warn("Exception during logging", e); + } + } + catch (ReflectionException e) + { + if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) + { + ChainingUserRegistrySynchronizer.logger + .warn("Exception during logging", e); + } + } + catch (MBeanException e) + { + if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) + { + ChainingUserRegistrySynchronizer.logger + .warn("Exception during logging", e); + } + } + catch (IOException e) + { + if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) + { + ChainingUserRegistrySynchronizer.logger + .warn("Exception during logging", e); + } + } + + } if (ChainingUserRegistrySynchronizer.logger.isInfoEnabled()) { ChainingUserRegistrySynchronizer.logger .info("Synchronizing users and groups with user registry '" + id + "'"); } - if (allowDeletions && ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) + if (isFullSync && ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) { ChainingUserRegistrySynchronizer.logger .warn("Full synchronization with user registry '" - + id - + "'; some users and groups previously created by synchronization with this user registry may be removed."); + + id + "'"); + if (allowDeletions) + { + ChainingUserRegistrySynchronizer.logger + .warn("Some users and groups previously created by synchronization with this user registry may be removed."); + } + else + { + ChainingUserRegistrySynchronizer.logger + .warn("Deletions are disabled. Users and groups removed from this registry will be logged only and will remain in the repository. Users previously found in a different registry will be moved in the repository rather than recreated."); + } } // Work out whether we should do the work in a separate transaction (it's most performant if we // bunch it into small transactions, but if we are doing a sync on login, it has to be the same @@ -432,13 +584,14 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl boolean requiresNew = splitTxns || AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_ONLY; - syncWithPlugin(id, plugin, forceUpdate, allowDeletions, requiresNew, visitedZoneIds, allZoneIds); + syncWithPlugin(id, plugin, forceUpdate, isFullSync, requiresNew, visitedZoneIds, allZoneIds); } } catch (NoSuchBeanDefinitionException e) { // Ignore and continue } + } } catch (RuntimeException e) @@ -583,7 +736,7 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl * the user registry and updated locally. When false then each source is only queried for * those users and groups modified since the most recent modification date of all the objects last * queried from that same source. - * @param allowDeletions + * @param isFullSync * Should a complete set of user and group IDs be queried from the user registries in order to determine * deletions? This parameter is independent of force as a separate query is run to process * updates. @@ -602,7 +755,7 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl * or group needs to be 're-zoned'. */ private void syncWithPlugin(final String zone, UserRegistry userRegistry, boolean forceUpdate, - boolean allowDeletions, boolean splitTxns, final Set visitedZoneIds, final Set allZoneIds) + boolean isFullSync, boolean splitTxns, final Set visitedZoneIds, final Set allZoneIds) { // Create a prefixed zone ID for use with the authority service final String zoneId = AuthorityService.ZONE_AUTH_EXT_PREFIX + zone; @@ -685,10 +838,24 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl // Check whether the group is in any of the authentication chain zones Set intersection = new TreeSet(groupZones); intersection.retainAll(allZoneIds); - if (intersection.isEmpty()) + // Check whether the group is in any of the higher priority authentication chain zones + Set visited = new TreeSet(intersection); + visited.retainAll(visitedZoneIds); + + if (groupZones.contains(zoneId)) { - // The group exists, but not in a zone that's in the authentication chain. May be due to - // upgrade or zone changes. Let's re-zone them + // The group already existed in this zone: update the group + updateGroup(group, true); + } + else if (!visited.isEmpty()) + { + // A group that exists in a different zone with higher precedence + return; + } + else if (!allowDeletions || intersection.isEmpty()) + { + // Deletions are disallowed or the group exists, but not in a zone that's in the authentication + // chain. May be due to upgrade or zone changes. Let's re-zone them if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) { ChainingUserRegistrySynchronizer.logger.warn("Updating group '" + groupShortName @@ -698,21 +865,12 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl ChainingUserRegistrySynchronizer.this.authorityService.removeAuthorityFromZones(groupName, groupZones); ChainingUserRegistrySynchronizer.this.authorityService.addAuthorityToZones(groupName, zoneSet); - } - if (groupZones.contains(zoneId) || intersection.isEmpty()) - { - // The group already existed in this zone or no valid zone: update the group + + // The group now exists in this zone: update the group updateGroup(group, true); } else { - // Check whether the group is in any of the higher priority authentication chain zones - intersection.retainAll(visitedZoneIds); - if (!intersection.isEmpty()) - { - // A group that exists in a different zone with higher precedence - return; - } // The group existed, but in a zone with lower precedence if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) { @@ -824,8 +982,6 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl { if (!newChildPersons.remove(child)) { - // Make sure each person with association changes features as a key in the creation map - recordParentAssociationCreation(child, null); recordParentAssociationDeletion(child, groupName); } } @@ -849,10 +1005,14 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl // Create new associations for (String child : newChildPersons) { + // Make sure each person with association changes features as a key in the deletion map + recordParentAssociationDeletion(child, null); recordParentAssociationCreation(child, groupName); } for (String child : newChildGroups) { + // Make sure each group with association changes features as a key in the deletion map + recordParentAssociationDeletion(child, null); recordParentAssociationCreation(child, groupName); } } @@ -1094,11 +1254,11 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl } } - public void processGroups(UserRegistry userRegistry, boolean allowDeletions, boolean splitTxns) + public void processGroups(UserRegistry userRegistry, boolean isFullSync, boolean splitTxns) { // If we got back some groups, we have to cross reference them with the set of known authorities - if (allowDeletions || !this.groupParentAssocsToCreate.isEmpty() - || !this.personParentAssocsToCreate.isEmpty()) + if (isFullSync || !this.groupParentAssocsToDelete.isEmpty() + || !this.groupParentAssocsToDelete.isEmpty()) { final Set allZonePersons = newPersonSet(); final Set allZoneGroups = new TreeSet(); @@ -1117,17 +1277,19 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl } }, true, splitTxns); - final Set personDeletionCandidates = newPersonSet(); - personDeletionCandidates.addAll(allZonePersons); - - final Set groupDeletionCandidates = new TreeSet(); - groupDeletionCandidates.addAll(allZoneGroups); - allZoneGroups.addAll(this.groupsToCreate.keySet()); // Prune our set of authorities according to deletions - if (allowDeletions) + if (isFullSync) { + final Set personDeletionCandidates = newPersonSet(); + personDeletionCandidates.addAll(allZonePersons); + + final Set groupDeletionCandidates = new TreeSet(); + groupDeletionCandidates.addAll(allZoneGroups); + + this.deletionCandidates = new TreeSet(); + for (String person : userRegistry.getPersonNames()) { personDeletionCandidates.remove(person); @@ -1141,14 +1303,80 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl this.deletionCandidates = new TreeSet(); this.deletionCandidates.addAll(personDeletionCandidates); this.deletionCandidates.addAll(groupDeletionCandidates); + if (allowDeletions) + { + allZonePersons.removeAll(personDeletionCandidates); + allZoneGroups.removeAll(groupDeletionCandidates); + } + else + { + if (!personDeletionCandidates.isEmpty()) + { + ChainingUserRegistrySynchronizer.logger.warn("The following missing users are not being deleted as allowDeletions == false"); + for (String person : personDeletionCandidates) + { + ChainingUserRegistrySynchronizer.logger.warn(" " + person); + } + } + if (!groupDeletionCandidates.isEmpty()) + { + ChainingUserRegistrySynchronizer.logger.warn("The following missing groups are not being deleted as allowDeletions == false"); + for (String group : groupDeletionCandidates) + { + ChainingUserRegistrySynchronizer.logger.warn(" " + group); + } + } + + // Complete association deletion information by scanning deleted groups + BatchProcessor groupScanner = new BatchProcessor(zone + + " Missing Authority Scanning", + ChainingUserRegistrySynchronizer.this.transactionService + .getRetryingTransactionHelper(), this.deletionCandidates, + ChainingUserRegistrySynchronizer.this.workerThreads, 20, + ChainingUserRegistrySynchronizer.this.applicationEventPublisher, + ChainingUserRegistrySynchronizer.logger, + ChainingUserRegistrySynchronizer.this.loggingInterval); + groupScanner.process(new BaseBatchProcessWorker() + { - allZonePersons.removeAll(personDeletionCandidates); - allZoneGroups.removeAll(groupDeletionCandidates); + @Override + public String getIdentifier(String entry) + { + return entry; + } + + @Override + public void process(String authority) throws Throwable + { + // Disassociate it from this zone, allowing it to be reclaimed by something further down the chain + ChainingUserRegistrySynchronizer.this.authorityService.removeAuthorityFromZones(authority, + Collections.singleton(zoneId)); + + // For groups, remove all members + if (AuthorityType.getAuthorityType(authority) != AuthorityType.USER) + { + String groupShortName = ChainingUserRegistrySynchronizer.this.authorityService + .getShortName(authority); + String groupDisplayName = ChainingUserRegistrySynchronizer.this.authorityService + .getAuthorityDisplayName(authority); + NodeDescription dummy = new NodeDescription(groupShortName + " (Deleted)"); + PropertyMap dummyProperties = dummy.getProperties(); + dummyProperties.put(ContentModel.PROP_AUTHORITY_NAME, authority); + if (groupDisplayName != null) + { + dummyProperties.put(ContentModel.PROP_AUTHORITY_DISPLAY_NAME, groupDisplayName); + } + updateGroup(dummy, true); + } + } + }, splitTxns); + + } } // Prune the group associations now that we have complete information this.groupParentAssocsToCreate.keySet().retainAll(allZoneGroups); - logRetainParentAssociations(this.groupParentAssocsToDelete, allZoneGroups); + logRetainParentAssociations(this.groupParentAssocsToCreate, allZoneGroups); this.finalGroupChildAssocs.keySet().retainAll(allZoneGroups); // Pruning person associations will have to wait until we have passed over all persons and built up @@ -1234,17 +1462,17 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl } // Remove all the associations we have already dealt with - this.personParentAssocsToCreate.keySet().removeAll(this.personsProcessed); + this.personParentAssocsToDelete.keySet().removeAll(this.personsProcessed); // Filter out associations to authorities that simply can't exist (and log if debugging is enabled) logRetainParentAssociations(this.personParentAssocsToCreate, this.allZonePersons); // Update associations to persons not updated themselves - if (!this.personParentAssocsToCreate.isEmpty()) + if (!this.personParentAssocsToDelete.isEmpty()) { BatchProcessor>> groupCreator = new BatchProcessor>>( zone + " Person Association", ChainingUserRegistrySynchronizer.this.transactionService - .getRetryingTransactionHelper(), this.personParentAssocsToCreate.entrySet(), + .getRetryingTransactionHelper(), this.personParentAssocsToDelete.entrySet(), ChainingUserRegistrySynchronizer.this.workerThreads, 20, ChainingUserRegistrySynchronizer.this.applicationEventPublisher, ChainingUserRegistrySynchronizer.logger, @@ -1340,7 +1568,7 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl final Analyzer groupAnalyzer = new Analyzer(lastModifiedMillis); int groupProcessedCount = groupProcessor.process(groupAnalyzer, splitTxns); - groupAnalyzer.processGroups(userRegistry, allowDeletions, splitTxns); + groupAnalyzer.processGroups(userRegistry, isFullSync, splitTxns); // Process persons and their parent associations @@ -1413,10 +1641,19 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl // Check whether the user is in any of the authentication chain zones Set intersection = new TreeSet(zones); intersection.retainAll(allZoneIds); - if (intersection.size() == 0) + // Check whether the user is in any of the higher priority authentication chain zones + Set visited = new TreeSet(intersection); + visited.retainAll(visitedZoneIds); + if (visited.size() > 0) { - // The person exists, but not in a zone that's in the authentication chain. May be due - // to upgrade or zone changes. Let's re-zone them + // A person that exists in a different zone with higher precedence - ignore + return; + } + + else if (!allowDeletions || intersection.isEmpty()) + { + // The person exists, but in a different zone. Either deletions are disallowed or the zone is + // not in the authentication chain. May be due to upgrade or zone changes. Let's re-zone them if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) { ChainingUserRegistrySynchronizer.logger.warn("Updating user '" + personName @@ -1431,14 +1668,6 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl } else { - // Check whether the user is in any of the higher priority authentication chain zones - intersection.retainAll(visitedZoneIds); - if (intersection.size() > 0) - { - // A person that exists in a different zone with higher precedence - ignore - return; - } - // The person existed, but in a zone with lower precedence if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) { @@ -1491,7 +1720,7 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl // Delete authorities if we have complete information for the zone Set deletionCandidates = groupAnalyzer.getDeletionCandidates(); - if (allowDeletions && !deletionCandidates.isEmpty()) + if (isFullSync && allowDeletions && !deletionCandidates.isEmpty()) { BatchProcessor authorityDeletionProcessor = new BatchProcessor( zone + " Authority Deletion", this.transactionService.getRetryingTransactionHelper(), diff --git a/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizerTest.java b/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizerTest.java index 3da10f46d9..ae5d5545ff 100644 --- a/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizerTest.java +++ b/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizerTest.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 * @@ -208,7 +208,19 @@ public class ChainingUserRegistrySynchronizerTest extends TestCase */ public void tearDownTestUsersAndGroups() throws Exception { - // Wipe out everything that was in Z1 and Z2 + // Re-zone everything that may have gone astray + this.applicationContextManager.setUserRegistries(new MockUserRegistry("Z0", new NodeDescription[] + { + newPerson("U1"), newPerson("U2"), newPerson("U3"), newPerson("U4"), newPerson("U5"), newPerson("U6"), + newPerson("U7") + }, new NodeDescription[] + { + newGroup("G1"), newGroup("G2"), newGroup("G3"), newGroup("G4"), newGroup("G5"), newGroup("G6"), + newGroup("G7") + }), new MockUserRegistry("Z1", new NodeDescription[] {}, new NodeDescription[] {}), new MockUserRegistry("Z2", + new NodeDescription[] {}, new NodeDescription[] {})); + this.synchronizer.synchronize(true, true, true); + // Wipe out everything that was in Z0 - Z2 this.applicationContextManager.setUserRegistries(new MockUserRegistry("Z0", new NodeDescription[] {}, new NodeDescription[] {}), new MockUserRegistry("Z1", new NodeDescription[] {}, new NodeDescription[] {}), new MockUserRegistry("Z2", new NodeDescription[] {}, @@ -382,6 +394,53 @@ public class ChainingUserRegistrySynchronizerTest extends TestCase tearDownTestUsersAndGroups(); } + /** + * Tests a forced update of the test users and groups with deletions disabled. No users or groups should be deleted, + * whether or not they move registry. Groups that would have been deleted should have no members and should only be + * in the default zone. + * + * @throws Exception + * the exception + */ + public void testForcedUpdateWithoutDeletions() throws Exception + { + UserRegistrySynchronizer synchronizer = (UserRegistrySynchronizer) ChainingUserRegistrySynchronizerTest.context + .getBean("testUserRegistrySynchronizerPreventDeletions"); + setUpTestUsersAndGroups(); + this.applicationContextManager.setUserRegistries(new MockUserRegistry("Z0", new NodeDescription[] + { + newPerson("U2"), newPerson("U3"), newPerson("U4"), + }, new NodeDescription[] + { + newGroup("G1"), newGroup("G2"), + }), new MockUserRegistry("Z1", new NodeDescription[] + { + newPerson("U5"), newPerson("u6"), + }, new NodeDescription[] {}), new MockUserRegistry("Z2", new NodeDescription[] + { + newPerson("U6"), + }, new NodeDescription[] {})); + synchronizer.synchronize(true, true, true); + this.retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + + public Object execute() throws Throwable + { + assertExists("Z0", "U2"); + assertExists("Z0", "U3"); + assertExists("Z0", "U4"); + assertExists("Z1", "U5"); + assertExists("Z1", "u6"); + assertExists(null, "U1"); + assertExists(null, "U7"); + assertExists(null, "G5"); + assertExists(null, "G6"); + return null; + } + }, false, true); + tearDownTestUsersAndGroups(); + } + /** * Tests a forced update of the test users and groups where some of the users change their case and some groups * appear with different case. @@ -604,8 +663,17 @@ public class ChainingUserRegistrySynchronizerTest extends TestCase assertTrue(this.authorityService.authorityExists(longName)); // Check in correct zone - assertTrue(this.authorityService.getAuthorityZones(longName).contains( - AuthorityService.ZONE_AUTH_EXT_PREFIX + zone)); + if (zone == null) + { + assertEquals(Collections.singleton(AuthorityService.ZONE_APP_DEFAULT), this.authorityService + .getAuthorityZones(longName)); + } + else + { + assertTrue(this.authorityService.getAuthorityZones(longName).contains( + AuthorityService.ZONE_AUTH_EXT_PREFIX + zone)); + } + if (AuthorityType.getAuthorityType(longName).equals(AuthorityType.GROUP)) { // Check groups have expected members diff --git a/source/java/org/alfresco/repo/security/sync/UserRegistrySynchronizer.java b/source/java/org/alfresco/repo/security/sync/UserRegistrySynchronizer.java index 3944df3514..b48f827b55 100644 --- a/source/java/org/alfresco/repo/security/sync/UserRegistrySynchronizer.java +++ b/source/java/org/alfresco/repo/security/sync/UserRegistrySynchronizer.java @@ -52,7 +52,7 @@ public interface UserRegistrySynchronizer * the user registry and updated locally. When false then each source is only queried for * those users and groups modified since the most recent modification date of all the objects last * queried from that same source. - * @param allowDeletions + * @param isFullSync * Should a complete set of user and group IDs be queried from the user registries in order to determine * deletions? This parameter is independent of force as a separate query is run to process * updates. @@ -62,7 +62,7 @@ public interface UserRegistrySynchronizer * false, all users and groups are processed in the current transaction. This is required if * calling synchronously (e.g. in response to an authentication event in the same transaction). */ - public void synchronize(boolean forceUpdate, boolean allowDeletions, boolean splitTxns); + public void synchronize(boolean forceUpdate, boolean isFullSync, boolean splitTxns); /** * Gets the set of property names that are auto-mapped for the user with the given user name. These should remain diff --git a/source/java/org/alfresco/repo/security/sync/ldap/LDAPUserRegistry.java b/source/java/org/alfresco/repo/security/sync/ldap/LDAPUserRegistry.java index f3cce214b4..196a968709 100644 --- a/source/java/org/alfresco/repo/security/sync/ldap/LDAPUserRegistry.java +++ b/source/java/org/alfresco/repo/security/sync/ldap/LDAPUserRegistry.java @@ -571,6 +571,10 @@ public class LDAPUserRegistry implements UserRegistry, LDAPNameResolver, Initial } else { + if (LDAPUserRegistry.logger.isDebugEnabled()) + { + LDAPUserRegistry.logger.debug("Person DN recognized: " + nameAttribute.get()); + } personNames.add((String) nameAttribute.get()); } } @@ -614,6 +618,10 @@ public class LDAPUserRegistry implements UserRegistry, LDAPNameResolver, Initial else { String authority = "GROUP_" + (String) nameAttribute.get(); + if (LDAPUserRegistry.logger.isDebugEnabled()) + { + LDAPUserRegistry.logger.debug("Group DN recognized: " + authority); + } groupNames.add(authority); } } @@ -716,7 +724,11 @@ public class LDAPUserRegistry implements UserRegistry, LDAPNameResolver, Initial Attribute memAttribute = getRangeRestrictedAttribute(attributes, LDAPUserRegistry.this.memberAttributeName); int nextStart = LDAPUserRegistry.this.attributeBatchSize; - + if (LDAPUserRegistry.logger.isDebugEnabled()) + { + LDAPUserRegistry.logger.debug("Processing group: " + gid + + ", from source: " + group.getSourceId()); + } // Loop until we get to the end of the range while (memAttribute != null) { @@ -745,6 +757,10 @@ public class LDAPUserRegistry implements UserRegistry, LDAPNameResolver, Initial && (nameAttribute = nameAttributes .get(LDAPUserRegistry.this.userIdAttributeName)) != null) { + if (LDAPUserRegistry.logger.isDebugEnabled()) + { + LDAPUserRegistry.logger.debug("User DN recognized: " + nameAttribute.get()); + } childAssocs.add((String) nameAttribute.get()); continue; } @@ -754,6 +770,10 @@ public class LDAPUserRegistry implements UserRegistry, LDAPNameResolver, Initial && (nameAttribute = nameAttributes .get(LDAPUserRegistry.this.groupIdAttributeName)) != null) { + if (LDAPUserRegistry.logger.isDebugEnabled()) + { + LDAPUserRegistry.logger.debug("Group DN recognized: " + "GROUP_" + nameAttribute.get()); + } childAssocs.add("GROUP_" + nameAttribute.get()); continue; } @@ -793,7 +813,10 @@ public class LDAPUserRegistry implements UserRegistry, LDAPNameResolver, Initial continue; } } - + if (LDAPUserRegistry.logger.isDebugEnabled()) + { + LDAPUserRegistry.logger.debug("User DN recognized by directory lookup: " + nameAttribute.get()); + } childAssocs.add((String) nameAttribute.get()); continue; } @@ -815,6 +838,10 @@ public class LDAPUserRegistry implements UserRegistry, LDAPNameResolver, Initial continue; } } + if (LDAPUserRegistry.logger.isDebugEnabled()) + { + LDAPUserRegistry.logger.debug("Group DN recognized by directory lookup: " + "GROUP_" + nameAttribute.get()); + } childAssocs.add("GROUP_" + nameAttribute.get()); continue; } @@ -844,6 +871,10 @@ public class LDAPUserRegistry implements UserRegistry, LDAPNameResolver, Initial { // The member attribute didn't parse as a DN. So assume we have a group class like // posixGroup (FDS) that directly lists user names + if (LDAPUserRegistry.logger.isDebugEnabled()) + { + LDAPUserRegistry.logger.debug("Member DN recognized as posixGroup: " + attribute); + } childAssocs.add(attribute); } } @@ -1121,7 +1152,20 @@ public class LDAPUserRegistry implements UserRegistry, LDAPNameResolver, Initial SearchControls searchControls = new SearchControls(); searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); searchControls.setReturningAttributes(returningAttributes); - + if (LDAPUserRegistry.logger.isDebugEnabled()) + { + LDAPUserRegistry.logger.debug("Processing query"); + LDAPUserRegistry.logger.debug("Search base: " + searchBase); + LDAPUserRegistry.logger.debug(" Return result limit: " + searchControls.getCountLimit()); + LDAPUserRegistry.logger.debug(" DerefLink: " + searchControls.getDerefLinkFlag()); + LDAPUserRegistry.logger.debug(" Return named object: " + searchControls.getReturningObjFlag()); + LDAPUserRegistry.logger.debug(" Time limit for search: " + searchControls.getTimeLimit()); + LDAPUserRegistry.logger.debug(" Attributes to return: " + returningAttributes.length + " items."); + for (String ra : returningAttributes) + { + LDAPUserRegistry.logger.debug(" Attribute: " + ra); + } + } InitialDirContext ctx = null; try { @@ -1285,6 +1329,11 @@ public class LDAPUserRegistry implements UserRegistry, LDAPNameResolver, Initial public void process(SearchResult result) throws NamingException, ParseException { this.count++; + if (LDAPUserRegistry.logger.isDebugEnabled()) + { + String personName = result.getNameInNamespace(); + LDAPUserRegistry.logger.debug("Processing person: " + personName); + } } /* diff --git a/source/java/org/alfresco/repo/tenant/AbstractTenantRoutingContentStore.java b/source/java/org/alfresco/repo/tenant/AbstractTenantRoutingContentStore.java index 2e2c2cbf01..3979c436e9 100644 --- a/source/java/org/alfresco/repo/tenant/AbstractTenantRoutingContentStore.java +++ b/source/java/org/alfresco/repo/tenant/AbstractTenantRoutingContentStore.java @@ -50,7 +50,7 @@ public abstract class AbstractTenantRoutingContentStore extends AbstractRoutingC private SimpleCache singletonCache; // eg. for contentStore private final String KEY_CONTENT_STORE = "key.tenant.routing.content.store"; - public void setDefaultRootDir(String defaultRootDirectory) + public void setRootLocation(String defaultRootDirectory) { this.defaultRootDirectory = defaultRootDirectory; } @@ -70,8 +70,7 @@ public abstract class AbstractTenantRoutingContentStore extends AbstractRoutingC this.singletonCache = singletonCache; } - /* - * (non-Javadoc) + /* (non-Javadoc) * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context. * ApplicationContext) */ @@ -80,6 +79,12 @@ public abstract class AbstractTenantRoutingContentStore extends AbstractRoutingC this.applicationContext = applicationContext; } + @Override + public String getRootLocation() + { + return defaultRootDirectory; + } + @Override protected ContentStore selectWriteStore(ContentContext ctx) { @@ -149,7 +154,7 @@ public abstract class AbstractTenantRoutingContentStore extends AbstractRoutingC public void init() { - String rootDir = defaultRootDirectory; + String rootDir = getRootLocation(); Tenant tenant = tenantService.getTenant(tenantService.getCurrentUserDomain()); if (tenant != null) { @@ -177,10 +182,5 @@ public abstract class AbstractTenantRoutingContentStore extends AbstractRoutingC destroy(); } - public String getDefaultRootDir() - { - return this.defaultRootDirectory; - } - protected abstract ContentStore initContentStore(ApplicationContext ctx, String contentRoot); } diff --git a/source/java/org/alfresco/repo/tenant/MultiTAdminServiceImpl.java b/source/java/org/alfresco/repo/tenant/MultiTAdminServiceImpl.java index edaa02c9cf..44f984cd78 100644 --- a/source/java/org/alfresco/repo/tenant/MultiTAdminServiceImpl.java +++ b/source/java/org/alfresco/repo/tenant/MultiTAdminServiceImpl.java @@ -32,6 +32,7 @@ import net.sf.acegisecurity.providers.encoding.PasswordEncoder; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.repo.admin.RepoModelDefinition; +import org.alfresco.repo.content.ContentStore; import org.alfresco.repo.dictionary.DictionaryComponent; import org.alfresco.repo.domain.tenant.TenantAdminDAO; import org.alfresco.repo.domain.tenant.TenantEntity; @@ -85,7 +86,7 @@ public class MultiTAdminServiceImpl implements TenantAdminService, ApplicationCo protected DictionaryComponent dictionaryComponent; protected TenantAdminDAO tenantAdminDAO; protected PasswordEncoder passwordEncoder; - protected TenantRoutingFileContentStore tenantFileContentStore; + protected ContentStore tenantFileContentStore; private ThumbnailRegistry thumbnailRegistry; private WorkflowService workflowService; @@ -166,7 +167,7 @@ public class MultiTAdminServiceImpl implements TenantAdminService, ApplicationCo this.passwordEncoder = passwordEncoder; } - public void setTenantFileContentStore(TenantRoutingFileContentStore tenantFileContentStore) + public void setTenantFileContentStore(ContentStore tenantFileContentStore) { this.tenantFileContentStore = tenantFileContentStore; } @@ -259,9 +260,12 @@ public class MultiTAdminServiceImpl implements TenantAdminService, ApplicationCo // register dictionary - to allow enable/disable tenant callbacks register(dictionaryComponent); - // register file store - to allow enable/disable tenant callbacks - // note: tenantFileContentStore must be registed before dictionaryRepositoryBootstrap - register(tenantFileContentStore, 0); + if (tenantFileContentStore instanceof TenantDeployer) + { + // register file store - to allow enable/disable tenant callbacks + // note: tenantFileContentStore must be registed before dictionaryRepositoryBootstrap + register((TenantDeployer)tenantFileContentStore, 0); + } UserTransaction userTransaction = transactionService.getUserTransaction(); @@ -272,12 +276,18 @@ public class MultiTAdminServiceImpl implements TenantAdminService, ApplicationCo // bootstrap Tenant Service internal cache List tenants = getAllTenants(); - + int enabledCount = 0; int disabledCount = 0; for (Tenant tenant : tenants) { + if ((! (tenantFileContentStore instanceof AbstractTenantRoutingContentStore)) && (! tenantFileContentStore.getRootLocation().equals(tenant.getRootContentStoreDir()))) + { + // eg. MT will not work with replicating-content-services-context.sample if tenants are not co-mingled + throw new AlfrescoRuntimeException("MT: cannot start tenants - TenantRoutingContentStore is not configured AND not all tenants use co-mingled content store"); + } + if (tenant.isEnabled()) { // this will also call tenant deployers registered so far ... @@ -359,7 +369,11 @@ public class MultiTAdminServiceImpl implements TenantAdminService, ApplicationCo AuthenticationUtil.setFullyAuthenticatedUser(getSystemUser(tenantDomain)); dictionaryComponent.init(); - tenantFileContentStore.init(); + + if (tenantFileContentStore instanceof TenantDeployer) + { + ((TenantDeployer)tenantFileContentStore).init(); + } // create tenant-specific stores ImporterBootstrap userImporterBootstrap = (ImporterBootstrap)ctx.getBean("userBootstrap-mt"); @@ -367,14 +381,14 @@ public class MultiTAdminServiceImpl implements TenantAdminService, ApplicationCo ImporterBootstrap systemImporterBootstrap = (ImporterBootstrap)ctx.getBean("systemBootstrap-mt"); bootstrapSystemTenantStore(systemImporterBootstrap, tenantDomain); - + // deprecated ImporterBootstrap versionImporterBootstrap = (ImporterBootstrap)ctx.getBean("versionBootstrap-mt"); bootstrapVersionTenantStore(versionImporterBootstrap, tenantDomain); ImporterBootstrap version2ImporterBootstrap = (ImporterBootstrap)ctx.getBean("version2Bootstrap-mt"); bootstrapVersionTenantStore(version2ImporterBootstrap, tenantDomain); - + ImporterBootstrap spacesArchiveImporterBootstrap = (ImporterBootstrap)ctx.getBean("spacesArchiveBootstrap-mt"); bootstrapSpacesArchiveTenantStore(spacesArchiveImporterBootstrap, tenantDomain); @@ -444,7 +458,11 @@ public class MultiTAdminServiceImpl implements TenantAdminService, ApplicationCo AuthenticationUtil.setFullyAuthenticatedUser(getSystemUser(tenantDomain)); dictionaryComponent.init(); - tenantFileContentStore.init(); + + if (tenantFileContentStore instanceof TenantDeployer) + { + ((TenantDeployer)tenantFileContentStore).init(); + } // import tenant-specific stores importBootstrapUserTenantStore(tenantDomain, directorySource); @@ -1141,19 +1159,21 @@ public class MultiTAdminServiceImpl implements TenantAdminService, ApplicationCo private void initTenant(String tenantDomain, String rootContentStoreDir) { - validateTenantName(tenantDomain); - + validateTenantName(tenantDomain); + if (existsTenant(tenantDomain)) { throw new AlfrescoRuntimeException("Tenant already exists: " + tenantDomain); - } - - if (rootContentStoreDir == null) - { - rootContentStoreDir = tenantFileContentStore.getDefaultRootDir(); } - else + + if (rootContentStoreDir != null) { + if (! (tenantFileContentStore instanceof AbstractTenantRoutingContentStore)) + { + // eg. MT will not work with replicating-content-services-context.sample + throw new AlfrescoRuntimeException("MT: cannot initialse tenant - TenantRoutingContentStore is not configured AND tenant is not using co-mingled content store (ie. default root location)"); + } + File tenantRootDir = new File(rootContentStoreDir); if ((tenantRootDir.exists()) && (tenantRootDir.list().length != 0)) { @@ -1161,6 +1181,11 @@ public class MultiTAdminServiceImpl implements TenantAdminService, ApplicationCo } } + if (rootContentStoreDir == null) + { + rootContentStoreDir = tenantFileContentStore.getRootLocation(); + } + // init - need to enable tenant (including tenant service) before stores bootstrap TenantEntity tenantEntity = new TenantEntity(tenantDomain); tenantEntity.setEnabled(true); diff --git a/source/java/org/alfresco/repo/tenant/TenantRoutingContentStore.java b/source/java/org/alfresco/repo/tenant/TenantRoutingContentStore.java index 62c3224c4c..ef6d6f1ee2 100644 --- a/source/java/org/alfresco/repo/tenant/TenantRoutingContentStore.java +++ b/source/java/org/alfresco/repo/tenant/TenantRoutingContentStore.java @@ -24,5 +24,4 @@ package org.alfresco.repo.tenant; */ public interface TenantRoutingContentStore extends TenantDeployer { - public String getDefaultRootDir(); } diff --git a/source/java/org/alfresco/service/cmr/remoteconnector/RemoteConnectorClientException.java b/source/java/org/alfresco/service/cmr/remoteconnector/RemoteConnectorClientException.java new file mode 100644 index 0000000000..355794d931 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/remoteconnector/RemoteConnectorClientException.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2005-2012 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.service.cmr.remoteconnector; + +import java.io.IOException; + +/** + * An exception thrown when the remote server indicates that the + * client has made a mistake with the request. + * This exception is normally thrown for responses in the 4xx range, + * eg if a 404 (not found) is returned by the remote server. + * + * Provided that the response was not too large, the response from + * the server will also be available. + * + * @author Nick Burch + * @since 4.0.3 + */ +public class RemoteConnectorClientException extends IOException +{ + private static final long serialVersionUID = -639209368873463536L; + private final int statusCode; + private final String statusText; + private final RemoteConnectorResponse response; + + public RemoteConnectorClientException(int statusCode, String statusText, + RemoteConnectorResponse response) + { + super(statusText); + this.statusCode = statusCode; + this.statusText = statusText; + this.response = response; + } + + public int getStatusCode() + { + return statusCode; + } + + public String getStatusText() + { + return statusText; + } + + public RemoteConnectorResponse getResponse() + { + return response; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/service/cmr/remoteconnector/RemoteConnectorResponse.java b/source/java/org/alfresco/service/cmr/remoteconnector/RemoteConnectorResponse.java index 21f6f835ba..39d2eec9c1 100644 --- a/source/java/org/alfresco/service/cmr/remoteconnector/RemoteConnectorResponse.java +++ b/source/java/org/alfresco/service/cmr/remoteconnector/RemoteConnectorResponse.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.InputStream; import org.apache.commons.httpclient.Header; +import org.springframework.extensions.webscripts.Status; /** * Helper wrapper around a Remote Response, for a request that @@ -37,6 +38,11 @@ public interface RemoteConnectorResponse */ RemoteConnectorRequest getRequest(); + /** + * @return The HTTP {@link Status} Code for the response + */ + int getStatus(); + /** * @return The raw response content type, if available */ diff --git a/source/java/org/alfresco/service/cmr/remoteconnector/RemoteConnectorServerException.java b/source/java/org/alfresco/service/cmr/remoteconnector/RemoteConnectorServerException.java new file mode 100644 index 0000000000..673e02e622 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/remoteconnector/RemoteConnectorServerException.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2005-2012 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.service.cmr.remoteconnector; + +import java.io.IOException; + +/** + * An exception thrown when the remote server indicates that it + * has encountered a problem with the request, and cannot process + * it. This typically means a 5xx response. + * + * @author Nick Burch + * @since 4.0.3 + */ +public class RemoteConnectorServerException extends IOException +{ + private static final long serialVersionUID = -639209368873463536L; + private final int statusCode; + private final String statusText; + + public RemoteConnectorServerException(int statusCode, String statusText) + { + super(statusText); + this.statusCode = statusCode; + this.statusText = statusText; + } + + public int getStatusCode() + { + return statusCode; + } + + public String getStatusText() + { + return statusText; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/service/cmr/remoteconnector/RemoteConnectorService.java b/source/java/org/alfresco/service/cmr/remoteconnector/RemoteConnectorService.java index 458b802de2..ba685eb638 100644 --- a/source/java/org/alfresco/service/cmr/remoteconnector/RemoteConnectorService.java +++ b/source/java/org/alfresco/service/cmr/remoteconnector/RemoteConnectorService.java @@ -44,9 +44,15 @@ public interface RemoteConnectorService RemoteConnectorRequest buildRequest(String url, String method); /** - * Executes the specified request, and return the response + * Executes the specified request, and return the response. + * + * @throws IOException If there was a problem with the communication to the server + * @throws AuthenticationException If the authentication details supplied were not accepted + * @throws RemoteConnectorClientException If the server indicates the client request was invalid + * @throws RemoteConnectorServerException If the server was itself unable to perform the request */ - RemoteConnectorResponse executeRequest(RemoteConnectorRequest request) throws IOException, AuthenticationException; + RemoteConnectorResponse executeRequest(RemoteConnectorRequest request) throws IOException, AuthenticationException, + RemoteConnectorClientException, RemoteConnectorServerException; /** * Executes the given request, requesting a JSON response, and diff --git a/source/java/org/alfresco/service/cmr/repository/ContentService.java b/source/java/org/alfresco/service/cmr/repository/ContentService.java index 50f517e2ff..4dba4f7fca 100644 --- a/source/java/org/alfresco/service/cmr/repository/ContentService.java +++ b/source/java/org/alfresco/service/cmr/repository/ContentService.java @@ -223,6 +223,25 @@ public interface ContentService @Auditable(parameters = {"sourceMimetype", "targetMimetype"}) public ContentTransformer getTransformer(String sourceMimetype, String targetMimetype); + /** + * Fetch the transformers that are capable of transforming the content in the + * given source mimetype to the given target mimetype with the provided transformation + * options. + *

+ * The transformation options provide a finer grain way of discovering the correct transformer, + * since the values and type of the options provided are considered by the transformer when + * deciding whether it can satisfy the transformation request. + * @param sourceUrl TODO + * @param sourceMimetype the source mimetype + * @param sourceSize the source size (bytes). Ignored if negative. + * @param targetMimetype the target mimetype + * @param options the transformation options + * + * @return ContentTransformer the transformers that can be used, or null if none are available + */ + @Auditable(parameters = {"sourceMimetype", "sourceSize", "targetMimetype", "options"}) + public List getTransformers(String sourceUrl, String sourceMimetype, long sourceSize, String targetMimetype, TransformationOptions options); + /** * Fetch the transformer that is capable of transforming the content in the * given source mimetype to the given target mimetype with the provided transformation @@ -261,32 +280,14 @@ public interface ContentService public long getMaxSourceSizeBytes(String sourceMimetype, String targetMimetype, TransformationOptions options); /** - * Fetch all the transformers that are capable of transforming the content in the - * given source mimetype to the given target mimetype with the provided transformation - * options. - *

- * The transformation options provide a finer grain way of discovering the correct transformer, - * since the values and type of the options provided are considered by the transformer when - * deciding whether it can satisfy the transformation request. - *

- * The list will contain all currently active, applicable transformers sorted in repository preference order. - * The contents of this list may change depending on such factors as the availability of particular transformers - * as well as their current behaviour. For these reasons, this list should not be cached. - * - * @param sourceMimetype the source mimetype - * @param sourceSize the source size (bytes). Ignored if negative. - * @param targetMimetype the target mimetype - * @param options the transformation options - * @return ContentTransformers a List of the transformers that can be used, or the empty list if none were available - * + * @deprecated use {@link #getTransformers(String, String, long, String, TransformationOptions). * @since 3.5 - * @see ContentAccessor#getMimetype() */ @Auditable(parameters = {"sourceMimetype", "sourceSize", "targetMimetype", "options"}) public List getActiveTransformers(String sourceMimetype, long sourceSize, String targetMimetype, TransformationOptions options); /** - * @deprecated use overloaded method with sourceSize parameter. + * @deprecated use {@link #getTransformers(String, String, long, String, TransformationOptions). */ public List getActiveTransformers(String sourceMimetype, String targetMimetype, TransformationOptions options); diff --git a/source/java/org/alfresco/service/cmr/repository/TransformationOptionLimits.java b/source/java/org/alfresco/service/cmr/repository/TransformationOptionLimits.java index 53a18c2de5..9ca790e1e5 100644 --- a/source/java/org/alfresco/service/cmr/repository/TransformationOptionLimits.java +++ b/source/java/org/alfresco/service/cmr/repository/TransformationOptionLimits.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2011 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -56,11 +56,20 @@ public class TransformationOptionLimits pages = new TransformationOptionPair(); } - private TransformationOptionLimits(TransformationOptionLimits a, TransformationOptionLimits b) + private TransformationOptionLimits(TransformationOptionLimits a, TransformationOptionLimits b, boolean lower) { - time = a.time.combine(b.time); - kbytes = a.kbytes.combine(b.kbytes); - pages = a.pages.combine(b.pages); + if (lower) + { + time = a.time.combine(b.time); + kbytes = a.kbytes.combine(b.kbytes); + pages = a.pages.combine(b.pages); + } + else + { + time = a.time.combineUpper(b.time); + kbytes = a.kbytes.combineUpper(b.kbytes); + pages = a.pages.combineUpper(b.pages); + } } // --------------- Time --------------- @@ -179,7 +188,22 @@ public class TransformationOptionLimits */ public TransformationOptionLimits combine(final TransformationOptionLimits that) { - return new TransformationOptionLimits(this, that) + return combine(that, true); + } + + /** + * Returns a TransformationOptionLimits that has getter methods that combine the + * the values from the getter methods of this and the supplied TransformationOptionLimits + * so that they return the lowest common denominator of the limits . + */ + public TransformationOptionLimits combineUpper(final TransformationOptionLimits that) + { + return combine(that, false); + } + + private TransformationOptionLimits combine(final TransformationOptionLimits that, boolean lower) + { + return new TransformationOptionLimits(this, that, lower) { @Override public void setTimeoutMs(long timeoutMs) diff --git a/source/java/org/alfresco/service/cmr/repository/TransformationOptionLimitsTest.java b/source/java/org/alfresco/service/cmr/repository/TransformationOptionLimitsTest.java index 9dbc520b10..3041fb057c 100644 --- a/source/java/org/alfresco/service/cmr/repository/TransformationOptionLimitsTest.java +++ b/source/java/org/alfresco/service/cmr/repository/TransformationOptionLimitsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2011 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -229,20 +229,85 @@ public class TransformationOptionLimitsTest @Test public void testCombine() throws Exception { - limits.setReadLimitTimeMs(123); // limit > - limits.setReadLimitKBytes(45); // limit < - limits.setMaxPages(789); // max = + limits.setReadLimitTimeMs(123); + limits.setReadLimitKBytes(45); + limits.setPageLimit(789); TransformationOptionLimits second = new TransformationOptionLimits(); - second.setTimeoutMs(12); // max < - second.setMaxSourceSizeKBytes(456); // max > - second.setMaxPages(789); // max = + second.setTimeoutMs(12); + second.setMaxSourceSizeKBytes(456); + second.setMaxPages(789); TransformationOptionLimits combined = limits.combine(second); assertEquals("Expected the lower value", 12, combined.getTimeoutMs()); // max < assertEquals("Expected the lower value", 45, combined.getReadLimitKBytes()); // limit < assertEquals("Expected the lower value", 789, combined.getMaxPages()); // max = + + assertEquals("Expected -1 as max is set", -1, combined.getReadLimitTimeMs()); // max < + assertEquals("Expected -1 as limit is set", -1, combined.getMaxSourceSizeKBytes()); // limit < + assertEquals("Expected -1 as limit is the same", -1, combined.getPageLimit()); // max = + } + + @Test + public void testCombineLimits() throws Exception + { + limits.setReadLimitTimeMs(123); + limits.setReadLimitKBytes(45); + limits.setPageLimit(789); + + TransformationOptionLimits second = new TransformationOptionLimits(); + second.setReadLimitTimeMs(12); + second.setReadLimitKBytes(-1); + second.setPageLimit(789); + + TransformationOptionLimits combined = limits.combine(second); + + assertEquals("Expected the lower value", 12, combined.getReadLimitTimeMs()); + assertEquals("Expected the lower value", 45, combined.getReadLimitKBytes()); + assertEquals("Expected the lower value", 789, combined.getPageLimit()); + } + + @Test + public void testCombineUpper() throws Exception + { + limits.setReadLimitTimeMs(123); + limits.setReadLimitKBytes(45); + limits.setPageLimit(789); + + TransformationOptionLimits second = new TransformationOptionLimits(); + second.setTimeoutMs(12); + second.setMaxSourceSizeKBytes(456); + second.setMaxPages(789); + + TransformationOptionLimits combined = limits.combineUpper(second); + + assertEquals("Expected -1 as only one max value was set", -1, combined.getTimeoutMs()); + assertEquals("Expected -1 as only one max value was set", -1, combined.getMaxSourceSizeKBytes()); + assertEquals("Expected -1 as only one max value was set", -1, combined.getMaxPages()); + + assertEquals("Expected -1 as only one limit value was set", -1, combined.getReadLimitTimeMs()); + assertEquals("Expected -1 as only one limit value was set", -1, combined.getReadLimitKBytes()); + assertEquals("Expected -1 as only one limit value was set", -1, combined.getPageLimit()); + } + + @Test + public void testCombineUpperLimits() throws Exception + { + limits.setReadLimitTimeMs(123); + limits.setReadLimitKBytes(45); + limits.setPageLimit(789); + + TransformationOptionLimits second = new TransformationOptionLimits(); + second.setReadLimitTimeMs(12); + second.setReadLimitKBytes(-1); + second.setPageLimit(789); + + TransformationOptionLimits combined = limits.combineUpper(second); + + assertEquals("Expected the higher value", 123, combined.getReadLimitTimeMs()); + assertEquals("Expected -1 as only one limit value was set", -1, combined.getReadLimitKBytes()); + assertEquals("Expected the higher value", 789, combined.getPageLimit()); } @Test diff --git a/source/java/org/alfresco/service/cmr/repository/TransformationOptionPair.java b/source/java/org/alfresco/service/cmr/repository/TransformationOptionPair.java index f355c888ef..31eb29f7c3 100644 --- a/source/java/org/alfresco/service/cmr/repository/TransformationOptionPair.java +++ b/source/java/org/alfresco/service/cmr/repository/TransformationOptionPair.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2011 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -128,6 +128,19 @@ public class TransformationOptionPair } return Math.min(value1, value2); } + + /** + * Returns the higher (common denominator) of the two value supplied. + * If either value is less than 0, -1 is returned. + */ + private long maxSet(long value1, long value2) + { + if (value1 < 0 || value2 < 0) + { + return -1; + } + return Math.max(value1, value2); + } public Map toMap(Map optionsMap, String optMaxKey, String optLimitKey) { @@ -161,24 +174,81 @@ public class TransformationOptionPair * Returns a TransformationOptionPair that has getter methods that combine the * the values from the getter methods of this and the supplied TransformationOptionPair. */ - public TransformationOptionPair combine(final TransformationOptionPair that) + public TransformationOptionPair combine(TransformationOptionPair that) + { + return combine(that, true); + } + + /** + * Returns a TransformationOptionPair that has getter methods that combine the + * the values from the getter methods of this and the supplied TransformationOptionPair + * so that they return the lowest common denominator of the two limits . + */ + public TransformationOptionPair combineUpper(final TransformationOptionPair that) + { + return combine(that, false); + } + + private TransformationOptionPair combine(final TransformationOptionPair that, final boolean lower) { return new TransformationOptionPair() { /** * Combines max values of this TransformationOptionPair and the supplied - * one to return the max to be used in a transformation. The limit + * one to return the max to be used in a transformation. When 'lower' the max * value is discarded (-1 is returned) if the combined limit value is lower. + * When 'not lower' (lowest common denominator) the max is only returned if the + * limit value is -1. */ @Override public long getMax() { - long max = minSet(TransformationOptionPair.this.getMax(), that.getMax()); - long limit = minSet(TransformationOptionPair.this.getLimit(), that.getLimit()); + long max = getMaxValue(); + long limit = getLimitValue(); - return (max >= 0 && (limit < 0 || limit >= max)) - ? max - : -1; + return lower + ? (max >= 0 && (limit < 0 || limit >= max)) + ? max + : -1 + : (limit < 0) + ? max + : -1; + } + + /** + * Combines limit values of this TransformationOptionPair and the supplied + * one to return the limit to be used in a transformation. When 'lower' the limit + * value is discarded (-1 is returned) if the combined max value is lower. + * When 'not lower' (lowest common denominator) the limit is only returned if the + * max value is -1. + */ + @Override + public long getLimit() + { + long max = getMaxValue(); + long limit = getLimitValue(); + + return lower + ? (limit >= 0 && (max < 0 || max > limit)) + ? limit + : -1 + : (max < 0) + ? limit + : -1; + } + + private long getLimitValue() + { + return lower + ? minSet(TransformationOptionPair.this.getLimit(), that.getLimit()) + : maxSet(TransformationOptionPair.this.getLimit(), that.getLimit()); + } + + private long getMaxValue() + { + return lower + ? minSet(TransformationOptionPair.this.getMax(), that.getMax()) + : maxSet(TransformationOptionPair.this.getMax(), that.getMax()); } @Override @@ -186,22 +256,6 @@ public class TransformationOptionPair { throw new UnsupportedOperationException(); } - - /** - * Combines limit values of this TransformationOptionPair and the supplied - * one to return the limit to be used in a transformation. The limit - * value is discarded (-1 is returned) if the combined max value is lower. - */ - @Override - public long getLimit() - { - long max = minSet(TransformationOptionPair.this.getMax(), that.getMax()); - long limit = minSet(TransformationOptionPair.this.getLimit(), that.getLimit()); - - return (limit >= 0 && (max < 0 || max >= limit)) - ? limit - : -1; - } @Override public void setLimit(long limit, String exceptionMessage) diff --git a/source/java/org/alfresco/util/ValueProtectingMap.java b/source/java/org/alfresco/util/ValueProtectingMap.java index a5c41ca000..21a80f56a5 100644 --- a/source/java/org/alfresco/util/ValueProtectingMap.java +++ b/source/java/org/alfresco/util/ValueProtectingMap.java @@ -1,441 +1,443 @@ -/* - * Copyright (C) 2005-2012 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.util; - -import java.io.Serializable; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.locks.ReentrantReadWriteLock; - -/** - * A map that protects keys and values from accidental modification. - *

- * Use this map when keys or values need to be protected against client modification. - * For example, when a component pulls a map from a common resource it can wrap - * the map with this class to prevent any accidental modification of the shared - * resource. - *

- * Upon first write to this map , the underlying map will be copied (selectively cloned), - * the original map handle will be discarded and the copied map will be used. Note that - * the map copy process will also occur if any mutable value is in danger of being - * exposed to client modification. Therefore, methods that iterate and retrieve values - * will also trigger the copy if any values are mutable. - * - * @param the map key type (must extend {@link Serializable}) - * @param the map value type (must extend {@link Serializable}) - * - * @author Derek Hulley - * @since 3.4.9 - * @since 4.0.1 - */ -public class ValueProtectingMap implements Map -{ - /** - * Default immutable classes: - *

  • String
  • - *
  • BigDecimal
  • - *
  • BigInteger
  • - *
  • Byte
  • - *
  • Double
  • - *
  • Float
  • - *
  • Integer
  • - *
  • Long
  • - *
  • Short
  • - *
  • Boolean
  • - *
  • Date
  • - *
  • Locale
  • - */ - public static final Set> DEFAULT_IMMUTABLE_CLASSES; - static - { - DEFAULT_IMMUTABLE_CLASSES = new HashSet>(13); - DEFAULT_IMMUTABLE_CLASSES.add(String.class); - DEFAULT_IMMUTABLE_CLASSES.add(BigDecimal.class); - DEFAULT_IMMUTABLE_CLASSES.add(BigInteger.class); - DEFAULT_IMMUTABLE_CLASSES.add(Byte.class); - DEFAULT_IMMUTABLE_CLASSES.add(Double.class); - DEFAULT_IMMUTABLE_CLASSES.add(Float.class); - DEFAULT_IMMUTABLE_CLASSES.add(Integer.class); - DEFAULT_IMMUTABLE_CLASSES.add(Long.class); - DEFAULT_IMMUTABLE_CLASSES.add(Short.class); - DEFAULT_IMMUTABLE_CLASSES.add(Boolean.class); - DEFAULT_IMMUTABLE_CLASSES.add(Date.class); - DEFAULT_IMMUTABLE_CLASSES.add(Locale.class); - } - - /** - * Protect a specific value if it is considered mutable - * - * @param the type of the value, which must be {@link Serializable} - * @param value the value to protect if it is mutable (may be null) - * @param immutableClasses a set of classes that can be considered immutable - * over and above the {@link #DEFAULT_IMMUTABLE_CLASSES default set} - * @return a cloned instance (via serialization) or the instance itself, if immutable - */ - @SuppressWarnings("unchecked") - public static S protectValue(S value, Set> immutableClasses) - { - if (!mustProtectValue(value, immutableClasses)) - { - return value; - } - // We have to clone it - // No worries about the return type; it has to be the same as we put into the serializer - return (S) SerializationUtils.deserialize(SerializationUtils.serialize(value)); - } - - /** - * Utility method to check if values need to be cloned or not - * - * @param the type of the value, which must be {@link Serializable} - * @param value the value to check - * @param immutableClasses a set of classes that can be considered immutable - * over and above the {@link #DEFAULT_IMMUTABLE_CLASSES default set} - * @return true if the value must NOT be given - * to the calling clients - */ - public static boolean mustProtectValue(S value, Set> immutableClasses) - { - if (value == null) - { - return false; - } - Class clazz = value.getClass(); - return ( - DEFAULT_IMMUTABLE_CLASSES.contains(clazz) == false && - immutableClasses.contains(clazz) == false); - } - - /** - * Utility method to clone a map, preserving immutable instances - * - * @param the map key type, which must be {@link Serializable} - * @param the map value type, which must be {@link Serializable} - * @param map the map to copy - * @param immutableClasses a set of classes that can be considered immutable - * over and above the {@link #DEFAULT_IMMUTABLE_CLASSES default set} - */ - public static Map cloneMap(Map map, Set> immutableClasses) - { - Map copy = new HashMap((int)(map.size() * 1.3)); - for (Map.Entry element : map.entrySet()) - { - K key = element.getKey(); - V value = element.getValue(); - // Clone as necessary - key = ValueProtectingMap.protectValue(key, immutableClasses); - value = ValueProtectingMap.protectValue(value, immutableClasses); - copy.put(key, value); - } - return copy; - } - - private ReentrantReadWriteLock.ReadLock readLock; - private ReentrantReadWriteLock.WriteLock writeLock; - - private boolean cloned = false; - private Map map; - private Set> immutableClasses; - - /** - * Construct providing a protected map and using only the - * {@link #DEFAULT_IMMUTABLE_CLASSES default immutable classes} - * - * @param protectedMap the map to safeguard - */ - public ValueProtectingMap(Map protectedMap) - { - this (protectedMap, null); - } - - /** - * Construct providing a protected map, complementing the set of - * {@link #DEFAULT_IMMUTABLE_CLASSES default immutable classes} - * - * @param protectedMap the map to safeguard - * @param immutableClasses additional immutable classes - * over and above the {@link #DEFAULT_IMMUTABLE_CLASSES default set} - * (may be null - */ - public ValueProtectingMap(Map protectedMap, Set> immutableClasses) - { - // Unwrap any internal maps if given a value protecting map - if (protectedMap instanceof ValueProtectingMap) - { - ValueProtectingMap mapTemp = (ValueProtectingMap) protectedMap; - this.map = mapTemp.map; - } - else - { - this.map = protectedMap; - } - - this.cloned = false; - if (immutableClasses == null) - { - this.immutableClasses = Collections.emptySet(); - } - else - { - this.immutableClasses = new HashSet>(immutableClasses); - } - // Construct locks - ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); - this.readLock = lock.readLock(); - this.writeLock = lock.writeLock(); - } - - /** - * An unsafe method to use for anything except tests. - * - * @return the map that this instance is protecting - */ - /* protected */ Map getProtectedMap() - { - return map; - } - - /** - * Called by methods that need to force the map into a safe state. - *

    - * This method can be called without any locks being active. - */ - private void cloneMap() - { - readLock.lock(); - try - { - // Check that it hasn't been copied already - if (cloned) - { - return; - } - } - finally - { - readLock.unlock(); - } - /* - * Note: This space here is a window during which some code could have made - * a copy. Therefore we will do a cautious double-check. - */ - // Put in a write lock before cloning the map - writeLock.lock(); - try - { - // Check that it hasn't been copied already - if (cloned) - { - return; - } - - Map copy = ValueProtectingMap.cloneMap(map, immutableClasses); - // Discard the original - this.map = copy; - this.cloned = true; - } - finally - { - writeLock.unlock(); - } - } - - /* - * READ-ONLY METHODS - */ - - @Override - public int size() - { - readLock.lock(); - try - { - return map.size(); - } - finally - { - readLock.unlock(); - } - } - - @Override - public boolean isEmpty() - { - readLock.lock(); - try - { - return map.isEmpty(); - } - finally - { - readLock.unlock(); - } - } - - @Override - public boolean containsKey(Object key) - { - readLock.lock(); - try - { - return map.containsKey(key); - } - finally - { - readLock.unlock(); - } - } - - @Override - public boolean containsValue(Object value) - { - readLock.lock(); - try - { - return map.containsValue(value); - } - finally - { - readLock.unlock(); - } - - } - - @Override - public int hashCode() - { - readLock.lock(); - try - { - return map.hashCode(); - } - finally - { - readLock.unlock(); - } - } - - @Override - public boolean equals(Object obj) - { - readLock.lock(); - try - { - return map.equals(obj); - } - finally - { - readLock.unlock(); - } - } - - @Override - public String toString() - { - readLock.lock(); - try - { - return map.toString(); - } - finally - { - readLock.unlock(); - } - } - - /* - * METHODS THAT *MIGHT* REQUIRE COPY - */ - - @Override - public V get(Object key) - { - readLock.lock(); - try - { - V value = map.get(key); - return ValueProtectingMap.protectValue(value, immutableClasses); - } - finally - { - readLock.unlock(); - } - } - - /* - * METHODS THAT REQUIRE COPY - */ - - @Override - public V put(K key, V value) - { - cloneMap(); - return map.put(key, value); - } - - @Override - public V remove(Object key) - { - cloneMap(); - return map.remove(key); - } - - @Override - public void putAll(Map m) - { - cloneMap(); - map.putAll(m); - } - - @Override - public void clear() - { - cloneMap(); - map.clear(); - } - - @Override - public Set keySet() - { - cloneMap(); - return map.keySet(); - } - - @Override - public Collection values() - { - cloneMap(); - return map.values(); - } - - @Override - public Set> entrySet() - { - cloneMap(); - return map.entrySet(); - } -} +/* + * Copyright (C) 2005-2012 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.util; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * A map that protects keys and values from accidental modification. + *

    + * Use this map when keys or values need to be protected against client modification. + * For example, when a component pulls a map from a common resource it can wrap + * the map with this class to prevent any accidental modification of the shared + * resource. + *

    + * Upon first write to this map , the underlying map will be copied (selectively cloned), + * the original map handle will be discarded and the copied map will be used. Note that + * the map copy process will also occur if any mutable value is in danger of being + * exposed to client modification. Therefore, methods that iterate and retrieve values + * will also trigger the copy if any values are mutable. + * + * @param the map key type (must extend {@link Serializable}) + * @param the map value type (must extend {@link Serializable}) + * + * @author Derek Hulley + * @since 3.4.9 + * @since 4.0.1 + */ +public class ValueProtectingMap implements Map, Serializable +{ + private static final long serialVersionUID = -9073485393875357605L; + + /** + * Default immutable classes: + *

  • String
  • + *
  • BigDecimal
  • + *
  • BigInteger
  • + *
  • Byte
  • + *
  • Double
  • + *
  • Float
  • + *
  • Integer
  • + *
  • Long
  • + *
  • Short
  • + *
  • Boolean
  • + *
  • Date
  • + *
  • Locale
  • + */ + public static final Set> DEFAULT_IMMUTABLE_CLASSES; + static + { + DEFAULT_IMMUTABLE_CLASSES = new HashSet>(13); + DEFAULT_IMMUTABLE_CLASSES.add(String.class); + DEFAULT_IMMUTABLE_CLASSES.add(BigDecimal.class); + DEFAULT_IMMUTABLE_CLASSES.add(BigInteger.class); + DEFAULT_IMMUTABLE_CLASSES.add(Byte.class); + DEFAULT_IMMUTABLE_CLASSES.add(Double.class); + DEFAULT_IMMUTABLE_CLASSES.add(Float.class); + DEFAULT_IMMUTABLE_CLASSES.add(Integer.class); + DEFAULT_IMMUTABLE_CLASSES.add(Long.class); + DEFAULT_IMMUTABLE_CLASSES.add(Short.class); + DEFAULT_IMMUTABLE_CLASSES.add(Boolean.class); + DEFAULT_IMMUTABLE_CLASSES.add(Date.class); + DEFAULT_IMMUTABLE_CLASSES.add(Locale.class); + } + + /** + * Protect a specific value if it is considered mutable + * + * @param the type of the value, which must be {@link Serializable} + * @param value the value to protect if it is mutable (may be null) + * @param immutableClasses a set of classes that can be considered immutable + * over and above the {@link #DEFAULT_IMMUTABLE_CLASSES default set} + * @return a cloned instance (via serialization) or the instance itself, if immutable + */ + @SuppressWarnings("unchecked") + public static S protectValue(S value, Set> immutableClasses) + { + if (!mustProtectValue(value, immutableClasses)) + { + return value; + } + // We have to clone it + // No worries about the return type; it has to be the same as we put into the serializer + return (S) SerializationUtils.deserialize(SerializationUtils.serialize(value)); + } + + /** + * Utility method to check if values need to be cloned or not + * + * @param the type of the value, which must be {@link Serializable} + * @param value the value to check + * @param immutableClasses a set of classes that can be considered immutable + * over and above the {@link #DEFAULT_IMMUTABLE_CLASSES default set} + * @return true if the value must NOT be given + * to the calling clients + */ + public static boolean mustProtectValue(S value, Set> immutableClasses) + { + if (value == null) + { + return false; + } + Class clazz = value.getClass(); + return ( + DEFAULT_IMMUTABLE_CLASSES.contains(clazz) == false && + immutableClasses.contains(clazz) == false); + } + + /** + * Utility method to clone a map, preserving immutable instances + * + * @param the map key type, which must be {@link Serializable} + * @param the map value type, which must be {@link Serializable} + * @param map the map to copy + * @param immutableClasses a set of classes that can be considered immutable + * over and above the {@link #DEFAULT_IMMUTABLE_CLASSES default set} + */ + public static Map cloneMap(Map map, Set> immutableClasses) + { + Map copy = new HashMap((int)(map.size() * 1.3)); + for (Map.Entry element : map.entrySet()) + { + K key = element.getKey(); + V value = element.getValue(); + // Clone as necessary + key = ValueProtectingMap.protectValue(key, immutableClasses); + value = ValueProtectingMap.protectValue(value, immutableClasses); + copy.put(key, value); + } + return copy; + } + + private ReentrantReadWriteLock.ReadLock readLock; + private ReentrantReadWriteLock.WriteLock writeLock; + + private boolean cloned = false; + private Map map; + private Set> immutableClasses; + + /** + * Construct providing a protected map and using only the + * {@link #DEFAULT_IMMUTABLE_CLASSES default immutable classes} + * + * @param protectedMap the map to safeguard + */ + public ValueProtectingMap(Map protectedMap) + { + this (protectedMap, null); + } + + /** + * Construct providing a protected map, complementing the set of + * {@link #DEFAULT_IMMUTABLE_CLASSES default immutable classes} + * + * @param protectedMap the map to safeguard + * @param immutableClasses additional immutable classes + * over and above the {@link #DEFAULT_IMMUTABLE_CLASSES default set} + * (may be null + */ + public ValueProtectingMap(Map protectedMap, Set> immutableClasses) + { + // Unwrap any internal maps if given a value protecting map + if (protectedMap instanceof ValueProtectingMap) + { + ValueProtectingMap mapTemp = (ValueProtectingMap) protectedMap; + this.map = mapTemp.map; + } + else + { + this.map = protectedMap; + } + + this.cloned = false; + if (immutableClasses == null) + { + this.immutableClasses = Collections.emptySet(); + } + else + { + this.immutableClasses = new HashSet>(immutableClasses); + } + // Construct locks + ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + this.readLock = lock.readLock(); + this.writeLock = lock.writeLock(); + } + + /** + * An unsafe method to use for anything except tests. + * + * @return the map that this instance is protecting + */ + /* protected */ Map getProtectedMap() + { + return map; + } + + /** + * Called by methods that need to force the map into a safe state. + *

    + * This method can be called without any locks being active. + */ + private void cloneMap() + { + readLock.lock(); + try + { + // Check that it hasn't been copied already + if (cloned) + { + return; + } + } + finally + { + readLock.unlock(); + } + /* + * Note: This space here is a window during which some code could have made + * a copy. Therefore we will do a cautious double-check. + */ + // Put in a write lock before cloning the map + writeLock.lock(); + try + { + // Check that it hasn't been copied already + if (cloned) + { + return; + } + + Map copy = ValueProtectingMap.cloneMap(map, immutableClasses); + // Discard the original + this.map = copy; + this.cloned = true; + } + finally + { + writeLock.unlock(); + } + } + + /* + * READ-ONLY METHODS + */ + + @Override + public int size() + { + readLock.lock(); + try + { + return map.size(); + } + finally + { + readLock.unlock(); + } + } + + @Override + public boolean isEmpty() + { + readLock.lock(); + try + { + return map.isEmpty(); + } + finally + { + readLock.unlock(); + } + } + + @Override + public boolean containsKey(Object key) + { + readLock.lock(); + try + { + return map.containsKey(key); + } + finally + { + readLock.unlock(); + } + } + + @Override + public boolean containsValue(Object value) + { + readLock.lock(); + try + { + return map.containsValue(value); + } + finally + { + readLock.unlock(); + } + + } + + @Override + public int hashCode() + { + readLock.lock(); + try + { + return map.hashCode(); + } + finally + { + readLock.unlock(); + } + } + + @Override + public boolean equals(Object obj) + { + readLock.lock(); + try + { + return map.equals(obj); + } + finally + { + readLock.unlock(); + } + } + + @Override + public String toString() + { + readLock.lock(); + try + { + return map.toString(); + } + finally + { + readLock.unlock(); + } + } + + /* + * METHODS THAT *MIGHT* REQUIRE COPY + */ + + @Override + public V get(Object key) + { + readLock.lock(); + try + { + V value = map.get(key); + return ValueProtectingMap.protectValue(value, immutableClasses); + } + finally + { + readLock.unlock(); + } + } + + /* + * METHODS THAT REQUIRE COPY + */ + + @Override + public V put(K key, V value) + { + cloneMap(); + return map.put(key, value); + } + + @Override + public V remove(Object key) + { + cloneMap(); + return map.remove(key); + } + + @Override + public void putAll(Map m) + { + cloneMap(); + map.putAll(m); + } + + @Override + public void clear() + { + cloneMap(); + map.clear(); + } + + @Override + public Set keySet() + { + cloneMap(); + return map.keySet(); + } + + @Override + public Collection values() + { + cloneMap(); + return map.values(); + } + + @Override + public Set> entrySet() + { + cloneMap(); + return map.entrySet(); + } +} diff --git a/source/java/org/alfresco/util/ValueProtectingMapTest.java b/source/java/org/alfresco/util/ValueProtectingMapTest.java index 14f32cc668..3b070dad2f 100644 --- a/source/java/org/alfresco/util/ValueProtectingMapTest.java +++ b/source/java/org/alfresco/util/ValueProtectingMapTest.java @@ -1,242 +1,264 @@ -/* - * Copyright (C) 2005-2012 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.util; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import junit.framework.TestCase; - -/** - * Tests {@link ValueProtectingMap} - * - * @author Derek Hulley - * @since 3.4.9 - * @since 4.0.1 - */ -public class ValueProtectingMapTest extends TestCase -{ - private static Set> moreImmutableClasses; - static - { - moreImmutableClasses = new HashSet>(13); - moreImmutableClasses.add(TestImmutable.class); - } - - /** - * A class that is immutable - */ - @SuppressWarnings("serial") - private static class TestImmutable implements Serializable - { - } - - /** - * A class that is mutable - */ - @SuppressWarnings("serial") - private static class TestMutable extends TestImmutable - { - public int i = 0; - public void increment() - { - i++; - } - @Override - public boolean equals(Object obj) - { - if (this == obj) return true; - if (obj == null) return false; - if (getClass() != obj.getClass()) return false; - TestMutable other = (TestMutable) obj; - if (i != other.i) return false; - return true; - } - } - - private List valueList; - private Map valueMap; - private Date valueDate; - private TestImmutable valueImmutable; - private TestMutable valueMutable; - - private ValueProtectingMap map; - private Map holyMap; - - @Override - protected void setUp() throws Exception - { - valueList = new ArrayList(4); - valueList.add("ONE"); - valueList.add("TWO"); - valueList.add("THREE"); - valueList.add("FOUR"); - valueList = Collections.unmodifiableList(valueList); - - valueMap = new HashMap(5); - valueMap.put("ONE", "ONE"); - valueMap.put("TWO", "TWO"); - valueMap.put("THREE", "THREE"); - valueMap.put("FOUR", "FOUR"); - valueMap = Collections.unmodifiableMap(valueMap); - - valueDate = new Date(); - - valueImmutable = new TestImmutable(); - valueMutable = new TestMutable(); - - holyMap = new HashMap(); - holyMap.put("DATE", valueDate); - holyMap.put("LIST", (Serializable) valueList); - holyMap.put("MAP", (Serializable) valueMap); - holyMap.put("IMMUTABLE", valueImmutable); - holyMap.put("MUTABLE", valueMutable); - - // Now wrap our 'holy' map so that it cannot be modified - holyMap = Collections.unmodifiableMap(holyMap); - - map = new ValueProtectingMap(holyMap, moreImmutableClasses); - } - - /** - * Make sure that NOTHING has changed in our 'holy' map - */ - private void checkMaps(boolean expectMapClone) - { - assertEquals("Holy map size is wrong: ", 5, holyMap.size()); - // Note that the immutability of the maps and lists means that we don't need - // to check every value within the lists and maps - if (expectMapClone) - { - // Make sure that the holy map has been released - assertTrue("Expect holy map to have been released: ", map.getProtectedMap() != holyMap); - // Do some updates to the backing map and ensure that they stick - Map mapClone = map.getProtectedMap(); - mapClone.put("ONE", "ONE"); - assertEquals("Modified the backing directly but value is not visible: ", map.get("ONE"), "ONE"); - map.put("TWO", "TWO"); - assertTrue("Backing map was changed again!", mapClone == map.getProtectedMap()); - mapClone.containsKey("TWO"); - } - else - { - // Make sure that the holy map is still acting as the backing map - assertTrue("Expect holy map to still be in use: ", map.getProtectedMap() == holyMap); - } - } - - public void testSetup() - { - checkMaps(false); - } - - /** - * No matter how many times we wrap instances in instances, the backing map must remain - * the same. - */ - public void testMapWrapping() - { - ValueProtectingMap mapTwo = new ValueProtectingMap(map); - assertTrue("Backing map must be shared: ", mapTwo.getProtectedMap() == map.getProtectedMap()); - ValueProtectingMap mapThree = new ValueProtectingMap(map); - assertTrue("Backing map must be shared: ", mapThree.getProtectedMap() == map.getProtectedMap()); - } - - public void testMapClear() - { - map.clear(); - assertEquals("Map should be empty: ", 0, map.size()); - checkMaps(true); - } - - public void testMapContainsKey() - { - assertTrue(map.containsKey("LIST")); - assertFalse(map.containsKey("LISTXXX")); - checkMaps(false); - } - - public void testMapContainsValue() - { - assertTrue(map.containsValue(valueMutable)); - assertFalse(map.containsValue("Dassie")); - checkMaps(false); - } - - public void testMapEntrySet() - { - map.entrySet(); - checkMaps(true); - } - - /** - * Ensures that single, immutable values are given out as-is and - * without affecting the backing storage - */ - public void testMapGetImmutable() - { - assertTrue("Immutable value instance incorrect", map.get("IMMUTABLE") == valueImmutable); - checkMaps(false); - } - - /** - * Ensures that single, immutable values are cloned before being given out - * without affecting the backing storage - */ - public void testMapGetMutable() - { - TestMutable mutable = (TestMutable) map.get("MUTABLE"); - assertFalse("Mutable value instance incorrect", mutable == valueMutable); - checkMaps(false); - // Modify the instance - mutable.increment(); - assertEquals("Backing mutable should not have changed: ", 0, valueMutable.i); - } - - public void testMapIsEmpty() - { - assertFalse(map.isEmpty()); - checkMaps(false); - } - - public void testMapKeySet() - { - map.keySet(); - checkMaps(true); - } - - public void testMapPut() - { - map.put("ANOTHER", "VALUE"); - checkMaps(true); - } - - public void testMapPutAll() - { - map.putAll(holyMap); - checkMaps(true); - } -} +/* + * Copyright (C) 2005-2012 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.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import junit.framework.TestCase; + +/** + * Tests {@link ValueProtectingMap} + * + * @author Derek Hulley + * @since 3.4.9 + * @since 4.0.1 + */ +public class ValueProtectingMapTest extends TestCase +{ + private static Set> moreImmutableClasses; + static + { + moreImmutableClasses = new HashSet>(13); + moreImmutableClasses.add(TestImmutable.class); + } + + /** + * A class that is immutable + */ + @SuppressWarnings("serial") + private static class TestImmutable implements Serializable + { + } + + /** + * A class that is mutable + */ + @SuppressWarnings("serial") + private static class TestMutable extends TestImmutable + { + public int i = 0; + public void increment() + { + i++; + } + @Override + public boolean equals(Object obj) + { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + TestMutable other = (TestMutable) obj; + if (i != other.i) return false; + return true; + } + } + + private List valueList; + private Map valueMap; + private Date valueDate; + private TestImmutable valueImmutable; + private TestMutable valueMutable; + + private ValueProtectingMap map; + private Map holyMap; + + @Override + protected void setUp() throws Exception + { + valueList = new ArrayList(4); + valueList.add("ONE"); + valueList.add("TWO"); + valueList.add("THREE"); + valueList.add("FOUR"); + valueList = Collections.unmodifiableList(valueList); + + valueMap = new HashMap(5); + valueMap.put("ONE", "ONE"); + valueMap.put("TWO", "TWO"); + valueMap.put("THREE", "THREE"); + valueMap.put("FOUR", "FOUR"); + valueMap = Collections.unmodifiableMap(valueMap); + + valueDate = new Date(); + + valueImmutable = new TestImmutable(); + valueMutable = new TestMutable(); + + holyMap = new HashMap(); + holyMap.put("DATE", valueDate); + holyMap.put("LIST", (Serializable) valueList); + holyMap.put("MAP", (Serializable) valueMap); + holyMap.put("IMMUTABLE", valueImmutable); + holyMap.put("MUTABLE", valueMutable); + + // Now wrap our 'holy' map so that it cannot be modified + holyMap = Collections.unmodifiableMap(holyMap); + + map = new ValueProtectingMap(holyMap, moreImmutableClasses); + } + + /** + * Make sure that NOTHING has changed in our 'holy' map + */ + private void checkMaps(boolean expectMapClone) + { + assertEquals("Holy map size is wrong: ", 5, holyMap.size()); + // Note that the immutability of the maps and lists means that we don't need + // to check every value within the lists and maps + if (expectMapClone) + { + // Make sure that the holy map has been released + assertTrue("Expect holy map to have been released: ", map.getProtectedMap() != holyMap); + // Do some updates to the backing map and ensure that they stick + Map mapClone = map.getProtectedMap(); + mapClone.put("ONE", "ONE"); + assertEquals("Modified the backing directly but value is not visible: ", map.get("ONE"), "ONE"); + map.put("TWO", "TWO"); + assertTrue("Backing map was changed again!", mapClone == map.getProtectedMap()); + mapClone.containsKey("TWO"); + } + else + { + // Make sure that the holy map is still acting as the backing map + assertTrue("Expect holy map to still be in use: ", map.getProtectedMap() == holyMap); + } + } + + public void testSetup() + { + checkMaps(false); + } + + /** + * No matter how many times we wrap instances in instances, the backing map must remain + * the same. + */ + public void testMapWrapping() + { + ValueProtectingMap mapTwo = new ValueProtectingMap(map); + assertTrue("Backing map must be shared: ", mapTwo.getProtectedMap() == map.getProtectedMap()); + ValueProtectingMap mapThree = new ValueProtectingMap(map); + assertTrue("Backing map must be shared: ", mapThree.getProtectedMap() == map.getProtectedMap()); + } + + public void testMapClear() + { + map.clear(); + assertEquals("Map should be empty: ", 0, map.size()); + checkMaps(true); + } + + public void testMapContainsKey() + { + assertTrue(map.containsKey("LIST")); + assertFalse(map.containsKey("LISTXXX")); + checkMaps(false); + } + + public void testMapContainsValue() + { + assertTrue(map.containsValue(valueMutable)); + assertFalse(map.containsValue("Dassie")); + checkMaps(false); + } + + public void testMapEntrySet() + { + map.entrySet(); + checkMaps(true); + } + + /** + * Ensures that single, immutable values are given out as-is and + * without affecting the backing storage + */ + public void testMapGetImmutable() + { + assertTrue("Immutable value instance incorrect", map.get("IMMUTABLE") == valueImmutable); + checkMaps(false); + } + + /** + * Ensures that single, immutable values are cloned before being given out + * without affecting the backing storage + */ + public void testMapGetMutable() + { + TestMutable mutable = (TestMutable) map.get("MUTABLE"); + assertFalse("Mutable value instance incorrect", mutable == valueMutable); + checkMaps(false); + // Modify the instance + mutable.increment(); + assertEquals("Backing mutable should not have changed: ", 0, valueMutable.i); + } + + public void testMapIsEmpty() + { + assertFalse(map.isEmpty()); + checkMaps(false); + } + + public void testMapKeySet() + { + map.keySet(); + checkMaps(true); + } + + public void testMapPut() + { + map.put("ANOTHER", "VALUE"); + checkMaps(true); + } + + public void testMapPutAll() + { + map.putAll(holyMap); + checkMaps(true); + } + + @SuppressWarnings("unchecked") + public void testSerializability() throws Exception + { + map.put("MORE", "STUFF"); + checkMaps(true); + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + ObjectOutputStream os = new ObjectOutputStream(baos); + os.writeObject(map); + os.close(); + // Read it back in + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bais); + ValueProtectingMap reloadedMap = (ValueProtectingMap) ois.readObject(); + ois.close(); + // Make sure it has the value + assertEquals("Reloaded object not same.", "STUFF", reloadedMap.get("MORE")); + } +} diff --git a/source/test-resources/sync-test-context.xml b/source/test-resources/sync-test-context.xml index 260e659320..82a828351a 100644 --- a/source/test-resources/sync-test-context.xml +++ b/source/test-resources/sync-test-context.xml @@ -32,6 +32,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + userRegistry + + + 100 + + + false + + + diff --git a/source/test-resources/tenant/mt-contentstore-context.xml b/source/test-resources/tenant/mt-contentstore-context.xml index eee37fd75e..9362c1ca1d 100644 --- a/source/test-resources/tenant/mt-contentstore-context.xml +++ b/source/test-resources/tenant/mt-contentstore-context.xml @@ -8,7 +8,7 @@ - +