From be167f60cf475f9e7301294188f21d5a01c61bf5 Mon Sep 17 00:00:00 2001 From: Derek Hulley Date: Tue, 17 Oct 2006 22:42:59 +0000 Subject: [PATCH] Merged V1.4 to HEAD svn merge svn://svn.alfresco.com:3691/alfresco/BRANCHES/V1.4@3987 svn://svn.alfresco.com:3691/alfresco/BRANCHES/V1.4@4133 . Removed LicenseComponent reference from projects\repository\source\java\org\alfresco\repo\descriptor\DescriptorServiceImpl.java git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@4135 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- config/alfresco/auditConfig.xml | 1 - config/alfresco/bootstrap-context.xml | 6 +- config/alfresco/core-services-context.xml | 22 +- .../post-create-indexes.sql | 3 + .../org.hibernate.dialect.Dialect/sample.sql | 4 - .../post-create-indexes.sql | 24 + .../AlfrescoSchemaMigrate-1.3.sql | 653 ---------------- .../AlfrescoSchemaMigrate-1.3.sql | 636 ---------------- .../AlfrescoSchemaUpdate-1.4-1.sql | 92 +++ .../AlfrescoSchemaUpdate-1.4-2.sql | 69 ++ config/alfresco/desktop/Alfresco.exe | Bin 53248 -> 327680 bytes config/alfresco/desktop/showDetails.js | 26 + config/alfresco/desktop/urlLink.js | 29 + ...mple => index-tracking-context.xml.sample} | 33 + config/alfresco/file-servers.xml | 27 +- config/alfresco/import-export-context.xml | 4 +- .../alfresco/messages/copy-service.properties | 3 + .../messages/patch-service.properties | 8 +- .../messages/schema-update.properties | 1 + .../workflow-interpreter-help.properties | 1 + .../messages/workflow-interpreter-help.txt | 174 +++++ config/alfresco/model/bpmModel.xml | 63 +- config/alfresco/network-protocol-context.xml | 1 + config/alfresco/public-services-context.xml | 8 + .../public-services-security-context.xml | 4 +- config/alfresco/repository.properties | 2 +- config/alfresco/rule-services-context.xml | 3 + .../templates/content/examples/show_audit.ftl | 173 +++++ .../templates/content_template_examples.xml | 14 + config/alfresco/workflow-context.xml | 33 +- config/alfresco/workflow/workflowModel.xml | 23 +- .../CAlfrescoApp/source/alfresco/Alfresco.cpp | 2 +- .../java/org/alfresco/filesys/CIFSServer.java | 50 +- .../java/org/alfresco/filesys/FTPServer.java | 51 +- .../alfresco/filesys/ftp/FTPSrvSession.java | 2 +- .../server/auth/AlfrescoAuthenticator.java | 193 ++++- .../server/auth/CifsAuthenticator.java | 2 +- .../filesys/server/auth/ClientInfo.java | 40 + .../auth/EnterpriseCifsAuthenticator.java | 2 +- .../auth/ntlm/AlfrescoAuthenticator.java | 427 ----------- .../server/auth/ntlm/NTLMLogonDetails.java | 17 + .../server/config/ServerConfiguration.java | 34 +- .../smb/server/repo/ContentDiskDriver.java | 64 +- .../server/repo/ContentIOControlHandler.java | 79 ++ .../smb/server/repo/DesktopAction.java | 85 ++- .../smb/server/repo/DesktopParams.java | 14 + .../repo/desk/JavaScriptDesktopAction.java | 5 + .../server/repo/desk/URLDesktopAction.java | 28 +- .../server/repo/pseudo/MemoryNetworkFile.java | 13 +- .../server/repo/pseudo/PseudoNetworkFile.java | 13 +- .../jcr/exporter/JCRSystemXMLExporter.java | 2 +- .../action/executer/CopyActionExecuter.java | 2 +- .../repo/admin/patch/AbstractPatch.java | 7 +- .../repo/admin/patch/PatchExecuter.java | 23 +- .../impl/AbstractPermissionChangePatch.java | 169 +++++ .../patch/impl/ActionRuleDecouplingPatch.java | 14 +- .../patch/impl/ContentPermissionPatch.java | 41 +- .../admin/patch/impl/PermissionDataPatch.java | 41 +- .../impl/UpdateGuestPermissionPatch.java | 27 +- .../admin/patch/util/ImportFileUpdater.java | 573 ++++++++++++++ .../alfresco/repo/copy/CopyServiceImpl.java | 43 +- .../repo/copy/CopyServiceImplTest.java | 27 + .../descriptor/DescriptorServiceImpl.java | 76 +- .../repo/descriptor/DescriptorStartupLog.java | 159 ++-- .../repo/dictionary/DictionaryDAOTest.java | 24 +- .../alfresco/repo/dictionary/TestModel.java | 2 + .../dictionary/dictionarydaotest_model.xml | 27 + .../repo/domain/hibernate/Permission.hbm.xml | 10 + .../repo/domain/hibernate/Transaction.hbm.xml | 31 +- .../repo/domain/schema/SchemaBootstrap.java | 275 ++++--- .../repo/importer/ImporterBootstrap.java | 23 +- .../system/SystemExporterImporter.java | 12 +- .../importer/system/SystemInfoBootstrap.java | 23 +- .../org/alfresco/repo/jscript/Actions.java | 154 ++-- .../alfresco/repo/jscript/CategoryNode.java | 206 +++++ .../repo/jscript/CategoryTemplateNode.java | 202 +++++ .../alfresco/repo/jscript/Classification.java | 144 ++++ .../java/org/alfresco/repo/jscript/Node.java | 26 +- .../repo/jscript/RhinoScriptService.java | 31 +- .../alfresco/repo/jscript/ScriptUtils.java | 56 ++ .../org/alfresco/repo/jscript/Session.java | 79 ++ .../filefolder/FileFolderServiceImpl.java | 57 +- .../alfresco/repo/node/db/NodeDaoService.java | 11 +- .../HibernateNodeDaoServiceImpl.java | 77 +- .../node/index/AbstractReindexComponent.java | 235 ++++++ .../index/FullIndexRecoveryComponent.java | 202 ++--- .../index/IndexRemoteTransactionTracker.java | 119 +++ .../IndexRemoteTransactionTrackerTest.java | 125 ++++ .../org/alfresco/repo/rule/BaseRuleTest.java | 3 +- .../repo/rule/RuleServiceCoverageTest.java | 3 +- .../alfresco/repo/rule/RuleServiceImpl.java | 180 +++-- .../repo/rule/RuleServiceImplTest.java | 70 +- .../impl/lucene/LuceneSearcherImpl2.java | 34 +- .../dynamic/LockOwnerDynamicAuthority.java | 12 +- .../permissions/impl/acegi/ACLEntryVoter.java | 52 +- .../repo/template/Classification.java | 123 +++ .../repo/template/FreeMarkerProcessor.java | 7 + .../org/alfresco/repo/template/Session.java | 50 ++ .../common/counter/VersionCounterService.java | 12 + .../repo/workflow/WorkflowDeployer.java | 23 +- .../repo/workflow/WorkflowInterpreter.java | 707 ++++++++++++++++++ .../repo/workflow/WorkflowPackageImpl.java | 9 + .../repo/workflow/jbpm/JBPMEngine.java | 23 +- .../repo/workflow/jbpm/JBPMEngineTest.java | 24 +- .../service/cmr/repository/CopyService.java | 26 +- .../service/cmr/repository/TemplateNode.java | 51 +- 106 files changed, 5379 insertions(+), 2646 deletions(-) create mode 100644 config/alfresco/dbscripts/create/1.4/org.hibernate.dialect.Dialect/post-create-indexes.sql delete mode 100644 config/alfresco/dbscripts/create/1.4/org.hibernate.dialect.Dialect/sample.sql create mode 100644 config/alfresco/dbscripts/create/1.4/org.hibernate.dialect.Oracle9Dialect/post-create-indexes.sql delete mode 100644 config/alfresco/dbscripts/upgrade/1.3/org.hibernate.dialect.MySQLInnoDBDialect/AlfrescoSchemaMigrate-1.3.sql delete mode 100644 config/alfresco/dbscripts/upgrade/1.3/org.hibernate.dialect.Oracle9Dialect/AlfrescoSchemaMigrate-1.3.sql create mode 100644 config/alfresco/dbscripts/upgrade/1.4/org.hibernate.dialect.Oracle9Dialect/AlfrescoSchemaUpdate-1.4-1.sql create mode 100644 config/alfresco/dbscripts/upgrade/1.4/org.hibernate.dialect.Oracle9Dialect/AlfrescoSchemaUpdate-1.4-2.sql create mode 100644 config/alfresco/desktop/showDetails.js create mode 100644 config/alfresco/desktop/urlLink.js rename config/alfresco/extension/{index-recovery-context.xml.sample => index-tracking-context.xml.sample} (53%) create mode 100644 config/alfresco/messages/copy-service.properties create mode 100644 config/alfresco/messages/workflow-interpreter-help.properties create mode 100644 config/alfresco/messages/workflow-interpreter-help.txt create mode 100644 config/alfresco/templates/content/examples/show_audit.ftl delete mode 100644 source/java/org/alfresco/filesys/server/auth/ntlm/AlfrescoAuthenticator.java create mode 100644 source/java/org/alfresco/repo/admin/patch/impl/AbstractPermissionChangePatch.java create mode 100644 source/java/org/alfresco/repo/admin/patch/util/ImportFileUpdater.java create mode 100644 source/java/org/alfresco/repo/jscript/CategoryNode.java create mode 100644 source/java/org/alfresco/repo/jscript/CategoryTemplateNode.java create mode 100644 source/java/org/alfresco/repo/jscript/Classification.java create mode 100644 source/java/org/alfresco/repo/jscript/ScriptUtils.java create mode 100644 source/java/org/alfresco/repo/jscript/Session.java create mode 100644 source/java/org/alfresco/repo/node/index/IndexRemoteTransactionTracker.java create mode 100644 source/java/org/alfresco/repo/node/index/IndexRemoteTransactionTrackerTest.java create mode 100644 source/java/org/alfresco/repo/template/Classification.java create mode 100644 source/java/org/alfresco/repo/template/Session.java create mode 100644 source/java/org/alfresco/repo/workflow/WorkflowInterpreter.java diff --git a/config/alfresco/auditConfig.xml b/config/alfresco/auditConfig.xml index e68bef07ee..f639baff74 100644 --- a/config/alfresco/auditConfig.xml +++ b/config/alfresco/auditConfig.xml @@ -2,7 +2,6 @@ - diff --git a/config/alfresco/bootstrap-context.xml b/config/alfresco/bootstrap-context.xml index 2d6444b057..7ea83ead81 100644 --- a/config/alfresco/bootstrap-context.xml +++ b/config/alfresco/bootstrap-context.xml @@ -35,7 +35,7 @@ - classpath:alfresco/dbscripts/create/1.4/${db.script.dialect}/sample.sql + classpath:alfresco/dbscripts/create/1.4/${db.script.dialect}/post-create-indexes.sql @@ -160,13 +160,13 @@ jbpm alfresco/workflow/review_processdefinition.xml text/xml - true + false jbpm alfresco/workflow/adhoc_processdefinition.xml text/xml - true + false diff --git a/config/alfresco/core-services-context.xml b/config/alfresco/core-services-context.xml index 46f76c6af6..c2a7980704 100644 --- a/config/alfresco/core-services-context.xml +++ b/config/alfresco/core-services-context.xml @@ -23,31 +23,25 @@ - - + + ${db.driver} - + ${db.url} - + ${db.username} ${db.password} - + ${db.pool.initial} - + ${db.pool.max} - - ${db.pool.maxIdleTime} - - - 1 - @@ -78,6 +72,7 @@ alfresco.messages.patch-service alfresco.messages.schema-update alfresco.messages.webdav-messages + alfresco.messages.copy-service @@ -490,6 +485,9 @@ + + + diff --git a/config/alfresco/dbscripts/create/1.4/org.hibernate.dialect.Dialect/post-create-indexes.sql b/config/alfresco/dbscripts/create/1.4/org.hibernate.dialect.Dialect/post-create-indexes.sql new file mode 100644 index 0000000000..d2bc282c82 --- /dev/null +++ b/config/alfresco/dbscripts/create/1.4/org.hibernate.dialect.Dialect/post-create-indexes.sql @@ -0,0 +1,3 @@ +-- +-- Add post-creation indexes. (Generic Schema 1.4) +-- diff --git a/config/alfresco/dbscripts/create/1.4/org.hibernate.dialect.Dialect/sample.sql b/config/alfresco/dbscripts/create/1.4/org.hibernate.dialect.Dialect/sample.sql deleted file mode 100644 index ba873b987c..0000000000 --- a/config/alfresco/dbscripts/create/1.4/org.hibernate.dialect.Dialect/sample.sql +++ /dev/null @@ -1,4 +0,0 @@ --- --- Insert post-creation scripts here --- This is a generic fallback for cases where specific dialects are not catered for --- \ No newline at end of file diff --git a/config/alfresco/dbscripts/create/1.4/org.hibernate.dialect.Oracle9Dialect/post-create-indexes.sql b/config/alfresco/dbscripts/create/1.4/org.hibernate.dialect.Oracle9Dialect/post-create-indexes.sql new file mode 100644 index 0000000000..826399ad97 --- /dev/null +++ b/config/alfresco/dbscripts/create/1.4/org.hibernate.dialect.Oracle9Dialect/post-create-indexes.sql @@ -0,0 +1,24 @@ +-- +-- Add post-creation indexes. (Oracle Schema 1.4) +-- +CREATE INDEX FKFFF41F9960601995 ON alf_access_control_entry (permission_id); +CREATE INDEX FKFFF41F99B25A50BF ON alf_access_control_entry (authority_id); +CREATE INDEX FKFFF41F99B9553F6C ON alf_access_control_entry (acl_id); +CREATE INDEX FK8A749A657B7FDE43 ON alf_auth_ext_keys (id); +CREATE INDEX FKFFC5468E74173FF4 ON alf_child_assoc (child_node_id); +CREATE INDEX FKFFC5468E8E50E582 ON alf_child_assoc (parent_node_id); +CREATE INDEX FK60EFB626B9553F6C ON alf_node (acl_id); +CREATE INDEX FK60EFB626D24ADD25 ON alf_node (protocol, identifier); +CREATE INDEX FK7D4CF8EC7F2C8017 ON alf_node_properties (node_id); +CREATE INDEX FKD654E027F2C8017 ON alf_node_aspects (node_id); +CREATE INDEX FKE1A550BCB69C43F3 ON alf_node_assoc (source_node_id); +CREATE INDEX FKE1A550BCA8FC7769 ON alf_node_assoc (target_node_id); +CREATE INDEX FK71C2002B7F2C8017 ON alf_node_status (node_id); +CREATE INDEX FKBD4FF53D22DBA5BA ON alf_store (root_node_id); + +-- +-- New audit tables +-- +CREATE INDEX FKEAD1817484342E39 ON alf_audit_fact (audit_date_id); +CREATE INDEX FKEAD18174A0F9B8D9 ON alf_audit_fact (audit_source_id); +CREATE INDEX FKEAD18174F524CFD7 ON alf_audit_fact (audit_conf_id); diff --git a/config/alfresco/dbscripts/upgrade/1.3/org.hibernate.dialect.MySQLInnoDBDialect/AlfrescoSchemaMigrate-1.3.sql b/config/alfresco/dbscripts/upgrade/1.3/org.hibernate.dialect.MySQLInnoDBDialect/AlfrescoSchemaMigrate-1.3.sql deleted file mode 100644 index 7d436ee983..0000000000 --- a/config/alfresco/dbscripts/upgrade/1.3/org.hibernate.dialect.MySQLInnoDBDialect/AlfrescoSchemaMigrate-1.3.sql +++ /dev/null @@ -1,653 +0,0 @@ --- ------------------------------------------------------ --- Alfresco Schema conversion V1.2.1 to V1.3 --- --- Author: Derek Hulley --- ------------------------------------------------------ - --- --- Create temporary 1.3 schema --- - -CREATE TABLE `T_access_control_entry` ( - `id` bigint(20) NOT NULL auto_increment, - `protocol` varchar(50) default NULL, - `identifier` varchar(100) default NULL, - `uuid` varchar(36) default NULL, - `typeUri` varchar(100) default NULL, - `typeName` varchar(100) default NULL, - `name` varchar(100) default NULL, - `recipient` varchar(100) default NULL, - `acl_id` bigint(20), - `permission_id` bigint(20), - `authority_id` varchar(100), - `allowed` bit(1) NOT NULL, - PRIMARY KEY (`id`) -); -ALTER TABLE `T_access_control_entry` ADD INDEX `IDX_REF`(`protocol`, `identifier`, `uuid`); - -CREATE TABLE `T_access_control_list` ( - `id` bigint(20) NOT NULL auto_increment, - `protocol` varchar(50) NOT NULL, - `identifier` varchar(100) NOT NULL, - `uuid` varchar(36) NOT NULL, - `inherits` bit(1) NOT NULL, - PRIMARY KEY (`id`) -); -ALTER TABLE `T_access_control_list` ADD INDEX `IDX_REF`(`protocol`, `identifier`, `uuid`); - -CREATE TABLE `T_applied_patch` ( - `id` varchar(32) NOT NULL, - `description` text, - `fixes_from_schema` int(11) default NULL, - `fixes_to_schema` int(11) default NULL, - `applied_to_schema` int(11) default NULL, - `target_schema` int(11) default NULL, - `applied_on_date` datetime default NULL, - `applied_to_server` varchar(64) default NULL, - `was_executed` bit(1) default NULL, - `succeeded` bit(1) default NULL, - `report` text -); - -CREATE TABLE `T_auth_ext_keys` ( - `id` varchar(100) NOT NULL, - `externalKey` varchar(100) NOT NULL -); - -CREATE TABLE `T_authority` ( - `recipient` varchar(100) NOT NULL -); - -CREATE TABLE `T_child_assoc` ( - `id` bigint(20) NOT NULL auto_increment, - `parent_node_id` bigint(20) default NULL, - `parent_protocol` varchar(50) default NULL, - `parent_identifier` varchar(100) default NULL, - `parent_uuid` varchar(36) default NULL, - `child_node_id` bigint(20) default NULL, - `child_protocol` varchar(50) default NULL, - `child_identifier` varchar(100) default NULL, - `child_uuid` varchar(36) default NULL, - `type_qname` varchar(255) NOT NULL, - `qname` varchar(255) NOT NULL, - `is_primary` bit(1) default NULL, - `assoc_index` int(11) default NULL, - PRIMARY KEY (`id`) -); -ALTER TABLE `T_child_assoc` ADD INDEX `IDX_REF_PARENT`(`parent_protocol`, `parent_identifier`, `parent_uuid`); -ALTER TABLE `T_child_assoc` ADD INDEX `IDX_REF_CHILD`(`child_protocol`, `child_identifier`, `child_uuid`); - -CREATE TABLE `T_node` ( - `id` bigint(20) NOT NULL auto_increment, - `protocol` varchar(50) NOT NULL, - `identifier` varchar(100) NOT NULL, - `uuid` varchar(36) NOT NULL, - `acl_id` bigint(20) default NULL, - `type_qname` varchar(255) NOT NULL, - PRIMARY KEY (`id`) -); -ALTER TABLE `T_node` ADD INDEX `IDX_REF`(`protocol`, `identifier`, `uuid`); - -CREATE TABLE `T_node_aspects` ( - `protocol` varchar(50) NOT NULL, - `identifier` varchar(100) NOT NULL, - `uuid` varchar(36) NOT NULL, - `node_id` bigint(20), - `qname` varchar(200) default NULL -); -ALTER TABLE `T_node_aspects` ADD INDEX `IDX_REF`(`protocol`, `identifier`, `uuid`); - -CREATE TABLE `T_node_assoc` ( - `id` bigint(20) NOT NULL auto_increment, - `source_node_id` bigint(20) default NULL, - `source_protocol` varchar(50) default NULL, - `source_identifier` varchar(100) default NULL, - `source_uuid` varchar(36) default NULL, - `target_node_id` bigint(20) default NULL, - `target_protocol` varchar(50) default NULL, - `target_identifier` varchar(100) default NULL, - `target_uuid` varchar(36) default NULL, - `type_qname` varchar(255) NOT NULL, - PRIMARY KEY (`id`) -); -ALTER TABLE `T_node_assoc` ADD INDEX `IDX_REF_SOURCE`(`source_protocol`, `source_identifier`, `source_uuid`); -ALTER TABLE `T_node_assoc` ADD INDEX `IDX_REF_TARGET`(`target_protocol`, `target_identifier`, `target_uuid`); - -CREATE TABLE `T_node_properties` ( - `protocol` varchar(50) NOT NULL, - `identifier` varchar(100) NOT NULL, - `uuid` varchar(36) NOT NULL, - `node_id` bigint(20), - `actual_type` varchar(15) NOT NULL, - `multi_valued` bit(1) NOT NULL, - `persisted_type` varchar(15) NOT NULL, - `boolean_value` bit(1) default NULL, - `long_value` bigint(20) default NULL, - `float_value` float default NULL, - `double_value` double default NULL, - `string_value` text, - `serializable_value` blob, - `qname` varchar(200) NOT NULL -); -ALTER TABLE `t_node_properties` ADD INDEX `IDX_REF`(`protocol`, `identifier`, `uuid`); - -CREATE TABLE `T_node_status` ( - `protocol` varchar(50) NOT NULL, - `identifier` varchar(100) NOT NULL, - `guid` varchar(36) NOT NULL, - `node_id` bigint(20) default NULL, - `change_txn_id` varchar(56) NOT NULL, - `deleted` bit(1) NOT NULL -); -ALTER TABLE `t_node_status` ADD INDEX `IDX_REF`(`protocol`, `identifier`, `guid`); - -CREATE TABLE `T_permission` ( - `id` bigint(20) NOT NULL auto_increment, - `type_qname` varchar(200) NOT NULL, - `name` varchar(100) NOT NULL, - PRIMARY KEY (`id`) -); - -CREATE TABLE `T_store` ( - `protocol` varchar(50) NOT NULL, - `identifier` varchar(100) NOT NULL, - `root_node_id` bigint(20) default NULL -); -ALTER TABLE `t_store` ADD INDEX `IDX_STORE_REF`(`protocol`, `identifier`); - -CREATE TABLE `T_version_count` ( - `protocol` varchar(50) NOT NULL, - `identifier` varchar(100) NOT NULL, - `version_count` int(11) NOT NULL -); - --- --- Copy data from old tables to intermediate tables --- - -insert into T_store (protocol, identifier) - select protocol, identifier from store; - -insert into T_node (protocol, identifier, uuid, type_qname) - select protocol, identifier, guid, type_qname from node; - -update T_store tstore set root_node_id = - (select tnode.id from T_node tnode where - tnode.protocol = tstore.protocol and - tnode.identifier = tstore.identifier and - tnode.uuid = - (select ostore.root_guid from store ostore where - ostore.protocol = tstore.protocol and - ostore.identifier = tstore.identifier - ) - ); - -insert into t_version_count (protocol, identifier, version_count) - select protocol, identifier, version_count from version_count; - -insert into t_node_status (protocol, identifier, guid, change_txn_id, deleted) - select protocol, identifier, guid, change_txn_id, deleted from node_status; -update T_node_status tstatus set node_id = - (select tnode.id from T_node tnode where - tnode.protocol = tstatus.protocol and - tnode.identifier = tstatus.identifier and - tnode.uuid = tstatus.guid - ); - -insert into T_node_properties - ( - protocol, identifier, uuid, actual_type, multi_valued, persisted_type, - boolean_value, long_value, float_value, double_value, string_value, serializable_value, qname - ) - select - protocol, identifier, guid, actual_type, multi_valued, persisted_type, - boolean_value, long_value, float_value, double_value, string_value, serializable_value, qname - from node_properties; -update T_node_properties tproperties set node_id = - (select tnode.id from T_node tnode where - tnode.protocol = tproperties.protocol and - tnode.identifier = tproperties.identifier and - tnode.uuid = tproperties.uuid - ); - -insert into T_node_aspects - ( - protocol, identifier, uuid, qname - ) - select - protocol, identifier, guid, qname - from node_aspects; -update T_node_aspects taspects set node_id = - (select tnode.id from T_node tnode where - tnode.protocol = taspects.protocol and - tnode.identifier = taspects.identifier and - tnode.uuid = taspects.uuid - ); - -insert into T_child_assoc - ( - parent_protocol, parent_identifier, parent_uuid, - child_protocol, child_identifier, child_uuid, - type_qname, qname, is_primary, assoc_index - ) - select - parent_protocol, parent_identifier, parent_guid, - child_protocol, child_identifier, child_guid, - type_qname, qname, isPrimary, assoc_index - from - child_assoc; -update T_child_assoc tassoc set parent_node_id = - (select tnode.id from T_node tnode where - tnode.protocol = tassoc.parent_protocol and - tnode.identifier = tassoc.parent_identifier and - tnode.uuid = tassoc.parent_uuid - ); -update T_child_assoc tassoc set child_node_id = - (select tnode.id from T_node tnode where - tnode.protocol = tassoc.child_protocol and - tnode.identifier = tassoc.child_identifier and - tnode.uuid = tassoc.child_uuid - ); - -insert into T_node_assoc - ( - source_protocol, source_identifier, source_uuid, - target_protocol, target_identifier, target_uuid, - type_qname - ) - select - source_protocol, source_identifier, source_guid, - target_protocol, target_identifier, target_guid, - type_qname - from - node_assoc; -update T_node_assoc tassoc set source_node_id = - (select tnode.id from T_node tnode where - tnode.protocol = tassoc.source_protocol and - tnode.identifier = tassoc.source_identifier and - tnode.uuid = tassoc.source_uuid - ); -update T_node_assoc tassoc set target_node_id = - (select tnode.id from T_node tnode where - tnode.protocol = tassoc.target_protocol and - tnode.identifier = tassoc.target_identifier and - tnode.uuid = tassoc.target_uuid - ); - -insert into T_permission - ( - type_qname, name - ) - select - CONCAT('{', type_uri, '}', type_name), name - from - permission_ref; - -insert into T_access_control_list - ( - protocol, identifier, uuid, inherits - ) - select - protocol, identifier, guid, inherits - from node_permission; -update T_node tnode set acl_id = - (select tacl.id from T_access_control_list tacl where - tacl.protocol = tnode.protocol and - tacl.identifier = tnode.identifier and - tacl.uuid = tnode.uuid - ); - -insert into T_auth_ext_keys - ( - id, externalKey - ) - select - id, externalKey - from - externalkeys; - -insert into T_authority - ( - recipient - ) - select - recipient - from - recipient; - -insert into T_access_control_entry - ( - protocol, identifier, uuid, - typeUri, typeName, name, - recipient, - allowed - ) - select - protocol, identifier, guid, - typeUri, typeName, name, - recipient, - allowed - from node_perm_entry; -update T_access_control_entry tentry - set - acl_id = - ( - select - tacl.id - from T_access_control_list tacl - join T_node tnode on tacl.id = tnode.acl_id - where - tnode.protocol = tentry.protocol and - tnode.identifier = tentry.identifier and - tnode.uuid = tentry.uuid - ); -update T_access_control_entry tentry - set - tentry.permission_id = - ( - select - tpermission.id - from T_permission tpermission - where - tpermission.type_qname = CONCAT('{', tentry.typeUri, '}', tentry.typeName) and - tpermission.name = tentry.name - ); -update T_access_control_entry tentry - set - tentry.authority_id = - ( - select - tauthority.recipient - from T_authority tauthority - where - tauthority.recipient = tentry.recipient - ); -delete from T_access_control_list where id not in (select distinct(acl_id) id from t_access_control_entry where acl_id is not null); -delete from T_access_control_entry where acl_id is null; -update T_node set acl_id = null where acl_id not in (select id from t_access_control_list); - --- --- Create New schema (MySQL) --- - -SET FOREIGN_KEY_CHECKS = 0; - -DROP TABLE child_assoc; -DROP TABLE node_assoc; -DROP TABLE node_properties; -DROP TABLE node_aspects; -DROP TABLE node; -DROP TABLE node_status; -DROP TABLE version_count; -DROP TABLE store; -DROP TABLE node_perm_entry; -DROP TABLE node_permission; -DROP TABLE permission_ref; -DROP TABLE recipient; -DROP TABLE externalKeys; - -CREATE TABLE `access_control_entry` ( - `id` bigint(20) NOT NULL auto_increment, - `acl_id` bigint(20) NOT NULL, - `permission_id` bigint(20) NOT NULL, - `authority_id` varchar(100) NOT NULL, - `allowed` bit(1) NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `acl_id` (`acl_id`,`permission_id`,`authority_id`), - KEY `FKF064DF7560601995` (`permission_id`), - KEY `FKF064DF75B25A50BF` (`authority_id`), - KEY `FKF064DF75B9553F6C` (`acl_id`), - CONSTRAINT `FKF064DF75B9553F6C` FOREIGN KEY (`acl_id`) REFERENCES `access_control_list` (`id`), - CONSTRAINT `FKF064DF7560601995` FOREIGN KEY (`permission_id`) REFERENCES `permission` (`id`), - CONSTRAINT `FKF064DF75B25A50BF` FOREIGN KEY (`authority_id`) REFERENCES `authority` (`recipient`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - -CREATE TABLE `access_control_list` ( - `id` bigint(20) NOT NULL auto_increment, - `inherits` bit(1) NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - -CREATE TABLE `auth_ext_keys` ( - `id` varchar(100) NOT NULL, - `externalKey` varchar(100) NOT NULL, - PRIMARY KEY (`id`,`externalKey`), - KEY `FK31D3BA097B7FDE43` (`id`), - CONSTRAINT `FK31D3BA097B7FDE43` FOREIGN KEY (`id`) REFERENCES `authority` (`recipient`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - -CREATE TABLE `authority` ( - `recipient` varchar(100) NOT NULL, - PRIMARY KEY (`recipient`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - -CREATE TABLE `child_assoc` ( - `id` bigint(20) NOT NULL auto_increment, - `parent_node_id` bigint(20) default NULL, - `child_node_id` bigint(20) default NULL, - `type_qname` varchar(255) NOT NULL, - `qname` varchar(255) NOT NULL, - `is_primary` bit(1) default NULL, - `assoc_index` int(11) default NULL, - PRIMARY KEY (`id`), - KEY `FKFFC5468E74173FF4` (`child_node_id`), - KEY `FKFFC5468E8E50E582` (`parent_node_id`), - CONSTRAINT `FKFFC5468E8E50E582` FOREIGN KEY (`parent_node_id`) REFERENCES `node` (`id`), - CONSTRAINT `FKFFC5468E74173FF4` FOREIGN KEY (`child_node_id`) REFERENCES `node` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -FKFFC5468E74173FF4 - -CREATE TABLE `node` ( - `id` bigint(20) NOT NULL auto_increment, - `protocol` varchar(50) NOT NULL, - `identifier` varchar(100) NOT NULL, - `uuid` varchar(36) NOT NULL, - `type_qname` varchar(255) NOT NULL, - `acl_id` bigint(20) default NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `protocol` (`protocol`,`identifier`,`uuid`), - KEY `FK33AE02D24ADD25` (`protocol`,`identifier`), - CONSTRAINT `FK33AE02D24ADD25` FOREIGN KEY (`protocol`, `identifier`) REFERENCES `store` (`protocol`, `identifier`), - CONSTRAINT `FK33AE02B9553F6C` FOREIGN KEY (`acl_id`) REFERENCES `access_control_list` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - -CREATE TABLE `node_aspects` ( - `node_id` bigint(20) NOT NULL, - `qname` varchar(200) default NULL, - KEY `FK2B91A9DE7F2C8017` (`node_id`), - CONSTRAINT `FK2B91A9DE7F2C8017` FOREIGN KEY (`node_id`) REFERENCES `node` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - -CREATE TABLE `node_assoc` ( - `id` bigint(20) NOT NULL auto_increment, - `source_node_id` bigint(20) default NULL, - `target_node_id` bigint(20) default NULL, - `type_qname` varchar(255) NOT NULL, - PRIMARY KEY (`id`), - KEY `FK5BAEF398B69C43F3` (`source_node_id`), - KEY `FK5BAEF398A8FC7769` (`target_node_id`), - CONSTRAINT `FK5BAEF398A8FC7769` FOREIGN KEY (`target_node_id`) REFERENCES `node` (`id`), - CONSTRAINT `FK5BAEF398B69C43F3` FOREIGN KEY (`source_node_id`) REFERENCES `node` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - -CREATE TABLE `node_properties` ( - `node_id` bigint(20) NOT NULL, - `actual_type` varchar(15) NOT NULL, - `multi_valued` bit(1) NOT NULL, - `persisted_type` varchar(15) NOT NULL, - `boolean_value` bit(1) default NULL, - `long_value` bigint(20) default NULL, - `float_value` float default NULL, - `double_value` double default NULL, - `string_value` text, - `serializable_value` blob, - `qname` varchar(200) NOT NULL, - PRIMARY KEY (`node_id`,`qname`), - KEY `FKC962BF907F2C8017` (`node_id`), - CONSTRAINT `FKC962BF907F2C8017` FOREIGN KEY (`node_id`) REFERENCES `node` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - -CREATE TABLE `node_status` ( - `protocol` varchar(50) NOT NULL, - `identifier` varchar(100) NOT NULL, - `guid` varchar(36) NOT NULL, - `node_id` bigint(20) default NULL, - `change_txn_id` varchar(56) NOT NULL, - PRIMARY KEY (`protocol`,`identifier`,`guid`), - KEY `FK38ECB8CF7F2C8017` (`node_id`), - CONSTRAINT `FK38ECB8CF7F2C8017` FOREIGN KEY (`node_id`) REFERENCES `node` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - -CREATE TABLE `permission` ( - `id` bigint(20) NOT NULL auto_increment, - `type_qname` varchar(200) NOT NULL, - `name` varchar(100) NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `type_qname` (`type_qname`,`name`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - -CREATE TABLE `store` ( - `protocol` varchar(50) NOT NULL, - `identifier` varchar(100) NOT NULL, - `root_node_id` bigint(20) default NULL, - PRIMARY KEY (`protocol`,`identifier`), - KEY `FK68AF8E122DBA5BA` (`root_node_id`), - CONSTRAINT `FK68AF8E122DBA5BA` FOREIGN KEY (`root_node_id`) REFERENCES `node` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - -CREATE TABLE `version_count` ( - `protocol` varchar(100) NOT NULL, - `identifier` varchar(100) NOT NULL, - `version_count` int(11) NOT NULL, - PRIMARY KEY (`protocol`,`identifier`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - --- --- Copy data into new schema --- - -insert into store - ( - protocol, identifier, root_node_id - ) - select - protocol, identifier, root_node_id - from - T_store; - -insert into node - ( - id, protocol, identifier, uuid, type_qname, acl_id - ) - select - id, protocol, identifier, uuid, type_qname, acl_id - from - T_node; - -insert into version_count - ( - protocol, identifier, version_count - ) - select - protocol, identifier, version_count - from - T_version_count; - -insert into node_status - ( - protocol, identifier, guid, node_id, change_txn_id - ) - select - protocol, identifier, guid, node_id, change_txn_id - from - T_node_status; - -insert into node_properties - ( - node_id, actual_type, multi_valued, persisted_type, - boolean_value, long_value, float_value, double_value, string_value, serializable_value, qname - ) - select - node_id, actual_type, multi_valued, persisted_type, - boolean_value, long_value, float_value, double_value, string_value, serializable_value, qname - from - T_node_properties; - -insert into node_aspects - ( - node_id, qname - ) - select - node_id, qname - from - T_node_aspects; - -insert into child_assoc - ( - id, parent_node_id, child_node_id, type_qname, qname, is_primary, assoc_index - ) - select - id, parent_node_id, child_node_id, type_qname, qname, is_primary, assoc_index - from - T_child_assoc; - -insert into node_assoc - ( - id, source_node_id, target_node_id, type_qname - ) - select - id, source_node_id, target_node_id, type_qname - from - T_node_assoc; - -insert into permission - ( - id, type_qname, name - ) - select - id, type_qname, name - from - T_permission; - -insert into access_control_list - ( - id, inherits - ) - select - id, inherits - from - T_access_control_list; - -insert into auth_ext_keys - ( - id, externalKey - ) - select - id, externalKey - from - T_auth_ext_keys; - -insert into authority - ( - recipient - ) - select - recipient - from - T_authority; - -insert into access_control_entry - ( - id, acl_id, permission_id, authority_id, allowed - ) - select - id, acl_id, permission_id, authority_id, allowed - from - T_access_control_entry; - -SET FOREIGN_KEY_CHECKS = 1; - - --- Allow longer patch identifiers - -ALTER TABLE applied_patch MODIFY id varchar(64) not null; \ No newline at end of file diff --git a/config/alfresco/dbscripts/upgrade/1.3/org.hibernate.dialect.Oracle9Dialect/AlfrescoSchemaMigrate-1.3.sql b/config/alfresco/dbscripts/upgrade/1.3/org.hibernate.dialect.Oracle9Dialect/AlfrescoSchemaMigrate-1.3.sql deleted file mode 100644 index d864606e25..0000000000 --- a/config/alfresco/dbscripts/upgrade/1.3/org.hibernate.dialect.Oracle9Dialect/AlfrescoSchemaMigrate-1.3.sql +++ /dev/null @@ -1,636 +0,0 @@ --- ------------------------------------------------------ --- Alfresco Schema conversion V1.2.1 to V1.3 --- --- For Oracle. --- --- Note: This script does not create a temporary --- properties table. It updates the existing --- table as it is not possible to insert..select --- long raw columns in Oracle. --- --- Author: David Caruana --- ------------------------------------------------------ - --- --- Create temporary 1.3 schema --- - -CREATE TABLE T_access_control_entry ( - id number(19,0) NOT NULL, - protocol varchar2(50) default NULL, - identifier varchar2(100) default NULL, - uuid varchar2(36) default NULL, - typeUri varchar2(100) default NULL, - typeName varchar2(100) default NULL, - name varchar2(100) default NULL, - recipient varchar2(100) default NULL, - acl_id number(19, 0), - permission_id number(19, 0), - authority_id varchar2(100), - allowed number(1, 0) NOT NULL, - PRIMARY KEY (id) -); -CREATE INDEX IDX_ACE_REF ON T_access_control_entry (protocol, identifier, uuid); - -CREATE TABLE T_access_control_list -( - id number(19,0) not null, - protocol varchar2(50) NOT NULL, - identifier varchar2(100) NOT NULL, - uuid varchar2(36) NOT NULL, - inherits number(1,0) NOT NULL, - PRIMARY KEY (id) -); -CREATE INDEX IDX_ACL_REF ON T_access_control_list (protocol, identifier, uuid); - -create table T_auth_ext_keys -( - id varchar2(100) not null, - externalKey varchar2(100) not null, - primary key (id, externalKey) -); - -create table T_authority -( - recipient varchar2(100) not null, - primary key (recipient) -); - -CREATE TABLE T_child_assoc -( - id number(19,0) NOT NULL, - parent_node_id number(19,0) default NULL, - parent_protocol varchar(50) default NULL, - parent_identifier varchar(100) default NULL, - parent_uuid varchar(36) default NULL, - child_node_id number(19,0) default NULL, - child_protocol varchar(50) default NULL, - child_identifier varchar(100) default NULL, - child_uuid varchar(36) default NULL, - type_qname varchar(255) NOT NULL, - qname varchar(255) NOT NULL, - is_primary number(1,0) default NULL, - assoc_index number(10,0) default NULL, - PRIMARY KEY (id) -); -CREATE INDEX IDX_CA_PARENT ON T_child_assoc(parent_protocol, parent_identifier, parent_uuid); -CREATE INDEX IDX_CA_CHILD ON T_child_assoc(child_protocol, child_identifier, child_uuid); - -CREATE TABLE T_node -( - id number(19,0) NOT NULL, - protocol varchar2(50) NOT NULL, - identifier varchar2(100) NOT NULL, - uuid varchar2(36) NOT NULL, - acl_id number(19,0) default NULL, - type_qname varchar2(255) NOT NULL, - PRIMARY KEY (id) -); -CREATE INDEX IDX_NODE_REF ON T_node(protocol, identifier, uuid); - -CREATE TABLE T_node_aspects -( - protocol varchar2(50) NOT NULL, - identifier varchar2(100) NOT NULL, - uuid varchar2(36) NOT NULL, - node_id number(19,0), - qname varchar2(200) default NULL -); -CREATE INDEX IDX_ASPECTS_REF ON T_node_aspects(protocol, identifier, uuid); - -CREATE TABLE T_node_assoc -( - id number(19,0) NOT NULL, - source_node_id number(19,0) default NULL, - source_protocol varchar2(50) default NULL, - source_identifier varchar2(100) default NULL, - source_uuid varchar2(36) default NULL, - target_node_id number(19,0) default NULL, - target_protocol varchar2(50) default NULL, - target_identifier varchar2(100) default NULL, - target_uuid varchar2(36) default NULL, - type_qname varchar2(255) NOT NULL, - PRIMARY KEY (id) -); -CREATE INDEX IDX_NA_SOURCE on T_node_assoc(source_protocol, source_identifier, source_uuid); -CREATE INDEX IDX_NA_TARGET on T_node_assoc(target_protocol, target_identifier, target_uuid); - -CREATE TABLE T_node_status -( - protocol varchar2(50) NOT NULL, - identifier varchar2(100) NOT NULL, - guid varchar2(36) NOT NULL, - node_id number(19,0) default NULL, - change_txn_id varchar2(56) NOT NULL, - deleted number(1,0) NOT NULL, - primary key (protocol, identifier, guid) -); - -CREATE TABLE T_permission -( - id number(19,0) NOT NULL, - type_qname varchar2(200) NOT NULL, - name varchar2(100) NOT NULL, - PRIMARY KEY (id), - unique (type_qname, name) -); - -CREATE TABLE T_store -( - protocol varchar2(50) NOT NULL, - identifier varchar2(100) NOT NULL, - root_node_id number(19,0) default NULL, - primary key (protocol, identifier) -); - -CREATE TABLE T_version_count -( - protocol varchar2(50) NOT NULL, - identifier varchar2(100) NOT NULL, - version_count number(10,0) NOT NULL, - primary key (protocol, identifier) -); - -create sequence hibernate_sequence; - - --- --- Copy data from old tables to intermediate tables --- - -insert into T_store (protocol, identifier) - select protocol, identifier from store; - -insert into T_node (id, protocol, identifier, uuid, type_qname) - select hibernate_sequence.nextval, protocol, identifier, guid, type_qname from node; - -update T_store tstore set root_node_id = - (select tnode.id from T_node tnode where - tnode.protocol = tstore.protocol and - tnode.identifier = tstore.identifier and - tnode.uuid = - (select ostore.root_guid from store ostore where - ostore.protocol = tstore.protocol and - ostore.identifier = tstore.identifier - ) - ); - -insert into t_version_count (protocol, identifier, version_count) - select protocol, identifier, version_count from version_count; - -insert into t_node_status (protocol, identifier, guid, change_txn_id, deleted) - select protocol, identifier, guid, change_txn_id, deleted from node_status; -update T_node_status tstatus set node_id = - (select tnode.id from T_node tnode where - tnode.protocol = tstatus.protocol and - tnode.identifier = tstatus.identifier and - tnode.uuid = tstatus.guid - ); - - -insert into T_node_aspects - ( - protocol, identifier, uuid, qname - ) - select - protocol, identifier, guid, qname - from node_aspects; -update T_node_aspects taspects set node_id = - (select tnode.id from T_node tnode where - tnode.protocol = taspects.protocol and - tnode.identifier = taspects.identifier and - tnode.uuid = taspects.uuid - ); - -insert into T_child_assoc - ( - id, parent_protocol, parent_identifier, parent_uuid, - child_protocol, child_identifier, child_uuid, - type_qname, qname, is_primary, assoc_index - ) - select - hibernate_sequence.nextval, parent_protocol, parent_identifier, parent_guid, - child_protocol, child_identifier, child_guid, - type_qname, qname, isPrimary, assoc_index - from - child_assoc; -update T_child_assoc tassoc set parent_node_id = - (select tnode.id from T_node tnode where - tnode.protocol = tassoc.parent_protocol and - tnode.identifier = tassoc.parent_identifier and - tnode.uuid = tassoc.parent_uuid - ); -update T_child_assoc tassoc set child_node_id = - (select tnode.id from T_node tnode where - tnode.protocol = tassoc.child_protocol and - tnode.identifier = tassoc.child_identifier and - tnode.uuid = tassoc.child_uuid - ); - -insert into T_node_assoc - ( - id, source_protocol, source_identifier, source_uuid, - target_protocol, target_identifier, target_uuid, - type_qname - ) - select - hibernate_sequence.nextval, source_protocol, source_identifier, source_guid, - target_protocol, target_identifier, target_guid, - type_qname - from - node_assoc; -update T_node_assoc tassoc set source_node_id = - (select tnode.id from T_node tnode where - tnode.protocol = tassoc.source_protocol and - tnode.identifier = tassoc.source_identifier and - tnode.uuid = tassoc.source_uuid - ); -update T_node_assoc tassoc set target_node_id = - (select tnode.id from T_node tnode where - tnode.protocol = tassoc.target_protocol and - tnode.identifier = tassoc.target_identifier and - tnode.uuid = tassoc.target_uuid - ); - -insert into T_permission - ( - id, type_qname, name - ) - select - hibernate_sequence.nextval, '{' || type_uri || '}' || type_name, name - from - permission_ref; - -insert into T_access_control_list - ( - id, protocol, identifier, uuid, inherits - ) - select - hibernate_sequence.nextval, protocol, identifier, guid, inherits - from node_permission; -update T_node tnode set acl_id = - (select tacl.id from T_access_control_list tacl where - tacl.protocol = tnode.protocol and - tacl.identifier = tnode.identifier and - tacl.uuid = tnode.uuid - ); - -insert into T_auth_ext_keys - ( - id, externalKey - ) - select - id, externalKey - from - externalkeys; - -insert into T_authority - ( - recipient - ) - select - recipient - from - recipient; - -insert into T_access_control_entry - ( - id, protocol, identifier, uuid, - typeUri, typeName, name, - recipient, - allowed - ) - select - hibernate_sequence.nextval, e.protocol, e.identifier, e.guid, - e.typeUri, e.typeName, e.name, - e.recipient, - e.allowed - from node_perm_entry e join t_node n on e.protocol = n.protocol and e.identifier = n.identifier and e.guid = n.uuid - ; - -update T_access_control_entry tentry - set - acl_id = - ( - select - tacl.id - from T_access_control_list tacl - join T_node tnode on tacl.id = tnode.acl_id - where - tnode.protocol = tentry.protocol and - tnode.identifier = tentry.identifier and - tnode.uuid = tentry.uuid - ); -update T_access_control_entry tentry - set - tentry.permission_id = - ( - select - tpermission.id - from T_permission tpermission - where - tpermission.type_qname = '{' || tentry.typeUri || '}' || tentry.typeName and - tpermission.name = tentry.name - ); -update T_access_control_entry tentry - set - tentry.authority_id = - ( - select - tauthority.recipient - from T_authority tauthority - where - tauthority.recipient = tentry.recipient - ); -delete from T_access_control_list where id not in (select distinct(acl_id) id from t_access_control_entry where acl_id is not null); -delete from T_access_control_entry where acl_id is null; -update T_node set acl_id = null where acl_id not in (select id from t_access_control_list); - --- --- Create New schema (Oracle) --- - -DROP TABLE child_assoc cascade constraints; -DROP TABLE node_assoc cascade constraints; -DROP TABLE node_aspects cascade constraints; -DROP TABLE node cascade constraints; -DROP TABLE node_status cascade constraints; -DROP TABLE version_count cascade constraints; -DROP TABLE store cascade constraints; -DROP TABLE node_perm_entry cascade constraints; -DROP TABLE node_permission cascade constraints; -DROP TABLE permission_ref cascade constraints; -DROP TABLE recipient cascade constraints; -DROP TABLE externalKeys cascade constraints; - -create table access_control_entry -( - id number(19,0) not null, - acl_id number(19,0) not null, - permission_id number(19,0) not null, - authority_id varchar2(100) not null, - allowed number(1,0) not null, - primary key (id), - unique (acl_id, permission_id, authority_id) -); - -create table access_control_list -( - id number(19,0) not null, - inherits number(1,0) not null, - primary key (id) -); - -create table auth_ext_keys -( - id varchar2(100) not null, - externalKey varchar2(100) not null, - primary key (id, externalKey) -); - -create table authority -( - recipient varchar2(100) not null, - primary key (recipient) -); - -create table child_assoc -( - id number(19,0) not null, - parent_node_id number(19,0), - child_node_id number(19,0), - type_qname varchar2(255) not null, - qname varchar2(255) not null, - is_primary number(1,0), - assoc_index number(10,0), - primary key (id) -); - -create table node -( - id number(19,0) not null, - protocol varchar2(50) not null, - identifier varchar2(100) not null, - uuid varchar2(36) not null, - type_qname varchar2(255) not null, - acl_id number(19,0), - primary key (id), - unique (protocol, identifier, uuid) -); - -create table node_aspects -( - node_id number(19,0) not null, - qname varchar2(200) -); - -create table node_assoc -( - id number(19,0) not null, - source_node_id number(19,0), - target_node_id number(19,0), - type_qname varchar2(255) not null, - primary key (id) -); - -create table node_status -( - protocol varchar2(50) not null, - identifier varchar2(100) not null, - guid varchar2(36) not null, - node_id number(19,0), - change_txn_id varchar2(56) not null, - primary key (protocol, identifier, guid) -); - -create table permission -( - id number(19,0) not null, - type_qname varchar2(200) not null, - name varchar2(100) not null, - primary key (id), - unique (type_qname, name) -); - -create table store -( - protocol varchar2(50) not null, - identifier varchar2(100) not null, - root_node_id number(19,0), - primary key (protocol, identifier) -); - -create table version_count -( - protocol varchar2(100) not null, - identifier varchar2(100) not null, - version_count number(10,0) not null, - primary key (protocol, identifier) -); - - --- --- Copy data into new schema --- - -insert into store - ( - protocol, identifier, root_node_id - ) - select - protocol, identifier, root_node_id - from - T_store; - -insert into node - ( - id, protocol, identifier, uuid, type_qname, acl_id - ) - select - id, protocol, identifier, uuid, type_qname, acl_id - from - T_node; - -insert into version_count - ( - protocol, identifier, version_count - ) - select - protocol, identifier, version_count - from - T_version_count; - -insert into node_status - ( - protocol, identifier, guid, node_id, change_txn_id - ) - select - protocol, identifier, guid, node_id, change_txn_id - from - T_node_status; - - -alter table node_properties add (node_id number(19,0)); - -update node_properties tproperties set node_id = - (select tnode.id from T_node tnode where - tnode.protocol = tproperties.protocol and - tnode.identifier = tproperties.identifier and - tnode.uuid = tproperties.guid - ); - -alter table node_properties modify (node_id number(19,0) not null); -alter table node_properties drop primary key; -alter table node_properties add primary key (node_id, qname); -alter table node_properties drop column protocol; -alter table node_properties drop column identifier; -alter table node_properties drop column guid; - - -insert into node_aspects - ( - node_id, qname - ) - select - node_id, qname - from - T_node_aspects; - -insert into child_assoc - ( - id, parent_node_id, child_node_id, type_qname, qname, is_primary, assoc_index - ) - select - id, parent_node_id, child_node_id, type_qname, qname, is_primary, assoc_index - from - T_child_assoc; - -insert into node_assoc - ( - id, source_node_id, target_node_id, type_qname - ) - select - id, source_node_id, target_node_id, type_qname - from - T_node_assoc; - -insert into permission - ( - id, type_qname, name - ) - select - id, type_qname, name - from - T_permission; - -insert into access_control_list - ( - id, inherits - ) - select - id, inherits - from - T_access_control_list; - -insert into auth_ext_keys - ( - id, externalKey - ) - select - id, externalKey - from - T_auth_ext_keys; - -insert into authority - ( - recipient - ) - select - recipient - from - T_authority; - -insert into access_control_entry - ( - id, acl_id, permission_id, authority_id, allowed - ) - select - id, acl_id, permission_id, authority_id, allowed - from - T_access_control_entry; - - --- Enable constraints - -alter table access_control_entry add constraint FKF064DF7560601995 foreign key (permission_id) references permission; -alter table access_control_entry add constraint FKF064DF75B25A50BF foreign key (authority_id) references authority; -alter table access_control_entry add constraint FKF064DF75B9553F6C foreign key (acl_id) references access_control_list; -alter table auth_ext_keys add constraint FK31D3BA097B7FDE43 foreign key (id) references authority; -alter table child_assoc add constraint FKC6EFFF3274173FF4 foreign key (child_node_id) references node; -alter table child_assoc add constraint FKC6EFFF328E50E582 foreign key (parent_node_id) references node; -alter table node add constraint FK33AE02B9553F6C foreign key (acl_id) references access_control_list; -alter table node add constraint FK33AE02D24ADD25 foreign key (protocol, identifier) references store; -alter table node_properties add constraint FKC962BF907F2C8017 foreign key (node_id) references node; -alter table node_aspects add constraint FK2B91A9DE7F2C8017 foreign key (node_id) references node; -alter table node_assoc add constraint FK5BAEF398B69C43F3 foreign key (source_node_id) references node; -alter table node_assoc add constraint FK5BAEF398A8FC7769 foreign key (target_node_id) references node; -alter table node_status add constraint FK38ECB8CF7F2C8017 foreign key (node_id) references node; -alter table store add constraint FK68AF8E122DBA5BA foreign key (root_node_id) references node; - --- Add additional indexes -CREATE INDEX FKF064DF7560601995 ON access_control_entry (permission_id); -CREATE INDEX FKF064DF75B25A50BF ON access_control_entry (authority_id); -CREATE INDEX FKF064DF75B9553F6C ON access_control_entry (acl_id); -CREATE INDEX FK31D3BA097B7FDE43 ON auth_ext_keys (id); -CREATE INDEX FKC6EFFF3274173FF4 ON child_assoc (child_node_id); -CREATE INDEX FKC6EFFF328E50E582 ON child_assoc (parent_node_id); -CREATE INDEX FK33AE02B9553F6C ON node (acl_id); -CREATE INDEX FK33AE02D24ADD25 ON node (protocol, identifier); -CREATE INDEX FK2B91A9DE7F2C8017 ON node_aspects (node_id); -CREATE INDEX FK5BAEF398B69C43F3 ON node_assoc (source_node_id); -CREATE INDEX FK5BAEF398A8FC7769 ON node_assoc (target_node_id); -CREATE INDEX FKC962BF907F2C8017 ON node_properties (node_id); -CREATE INDEX FK38ECB8CF7F2C8017 ON node_status (node_id); -CREATE INDEX FK68AF8E122DBA5BA ON store (root_node_id); - -ALTER TABLE applied_patch MODIFY id varchar(64); diff --git a/config/alfresco/dbscripts/upgrade/1.4/org.hibernate.dialect.Oracle9Dialect/AlfrescoSchemaUpdate-1.4-1.sql b/config/alfresco/dbscripts/upgrade/1.4/org.hibernate.dialect.Oracle9Dialect/AlfrescoSchemaUpdate-1.4-1.sql new file mode 100644 index 0000000000..f2fdd876b9 --- /dev/null +++ b/config/alfresco/dbscripts/upgrade/1.4/org.hibernate.dialect.Oracle9Dialect/AlfrescoSchemaUpdate-1.4-1.sql @@ -0,0 +1,92 @@ +-- ------------------------------------------------------ +-- Alfresco Schema conversion V1.3 to V1.4 Part 1 (Oracle) +-- +-- Adds the columns required to enforce the duplicate name detection +-- +-- Author: Derek Hulley +-- ------------------------------------------------------ + +-- +-- Unique name constraint +-- + +-- Apply new schema changes to child assoc table +ALTER TABLE child_assoc ADD + ( + child_node_name VARCHAR2(50 CHAR) DEFAULT 'V1.4 upgrade' NOT NULL, + child_node_name_crc NUMBER(19,0) DEFAULT -1 NOT NULL + ); + +UPDATE child_assoc + SET child_node_name_crc = id * -1; + +CREATE UNIQUE INDEX IDX_CHILD_NAMECRC ON child_assoc (parent_node_id, type_qname, child_node_name, child_node_name_crc); + +-- Apply unique index for node associations +CREATE UNIQUE INDEX IDX_ASSOC ON node_assoc (source_node_id, type_qname, target_node_id); + +-- +-- Rename tables to give 'alf_' prefix +-- +ALTER TABLE access_control_entry RENAME TO alf_access_control_entry; +ALTER TABLE access_control_list RENAME TO alf_access_control_list; +ALTER TABLE applied_patch RENAME TO alf_applied_patch; +ALTER TABLE auth_ext_keys RENAME TO alf_auth_ext_keys; +ALTER TABLE authority RENAME TO alf_authority; +ALTER TABLE child_assoc RENAME TO alf_child_assoc; +ALTER TABLE node RENAME TO alf_node; +ALTER TABLE node_aspects RENAME TO alf_node_aspects; +ALTER TABLE node_assoc RENAME TO alf_node_assoc; +ALTER TABLE node_properties RENAME TO alf_node_properties; +ALTER TABLE node_status RENAME TO alf_node_status; +ALTER TABLE permission RENAME TO alf_permission; +ALTER TABLE store RENAME TO alf_store; +ALTER TABLE version_count RENAME TO alf_version_count; + +-- +-- The table renames will cause Hibernate to rehash the FK constraint names +-- +ALTER TABLE alf_access_control_entry RENAME CONSTRAINT FKF064DF7560601995 TO FKFFF41F9960601995; +ALTER TABLE alf_access_control_entry RENAME CONSTRAINT FKF064DF75B25A50BF TO FKFFF41F99B25A50BF; +ALTER TABLE alf_access_control_entry RENAME CONSTRAINT FKF064DF75B9553F6C TO FKFFF41F99B9553F6C; +ALTER TABLE alf_auth_ext_keys RENAME CONSTRAINT FK31D3BA097B7FDE43 TO FK8A749A657B7FDE43; +ALTER TABLE alf_child_assoc RENAME CONSTRAINT FKC6EFFF3274173FF4 TO FKFFC5468E74173FF4; +ALTER TABLE alf_child_assoc RENAME CONSTRAINT FKC6EFFF328E50E582 TO FKFFC5468E8E50E582; +ALTER TABLE alf_node RENAME CONSTRAINT FK33AE02B9553F6C TO FK60EFB626B9553F6C; +ALTER TABLE alf_node RENAME CONSTRAINT FK33AE02D24ADD25 TO FK60EFB626D24ADD25; +ALTER TABLE alf_node_properties RENAME CONSTRAINT FKC962BF907F2C8017 TO FK7D4CF8EC7F2C8017; +ALTER TABLE alf_node_aspects RENAME CONSTRAINT FK2B91A9DE7F2C8017 TO FKD654E027F2C8017; +ALTER TABLE alf_node_assoc RENAME CONSTRAINT FK5BAEF398B69C43F3 TO FKE1A550BCB69C43F3; +ALTER TABLE alf_node_assoc RENAME CONSTRAINT FK5BAEF398A8FC7769 TO FKE1A550BCA8FC7769; +ALTER TABLE alf_node_status RENAME CONSTRAINT FK38ECB8CF7F2C8017 TO FK71C2002B7F2C8017; +ALTER TABLE alf_store RENAME CONSTRAINT FK68AF8E122DBA5BA TO FKBD4FF53D22DBA5BA; + +-- +-- Rename the indexes to keep in synch with the new table names. For Oracle, Hibernate doesn't create or add these +-- +ALTER INDEX FKF064DF7560601995 RENAME TO FKFFF41F9960601995; +ALTER INDEX FKF064DF75B25A50BF RENAME TO FKFFF41F99B25A50BF; +ALTER INDEX FKF064DF75B9553F6C RENAME TO FKFFF41F99B9553F6C; +ALTER INDEX FK31D3BA097B7FDE43 RENAME TO FK8A749A657B7FDE43; +ALTER INDEX FKC6EFFF3274173FF4 RENAME TO FKFFC5468E74173FF4; +ALTER INDEX FKC6EFFF328E50E582 RENAME TO FKFFC5468E8E50E582; +ALTER INDEX FK33AE02B9553F6C RENAME TO FK60EFB626B9553F6C; +ALTER INDEX FK33AE02D24ADD25 RENAME TO FK60EFB626D24ADD25; +ALTER INDEX FKC962BF907F2C8017 RENAME TO FK7D4CF8EC7F2C8017; +ALTER INDEX FK2B91A9DE7F2C8017 RENAME TO FKD654E027F2C8017; +ALTER INDEX FK5BAEF398B69C43F3 RENAME TO FKE1A550BCB69C43F3; +ALTER INDEX FK5BAEF398A8FC7769 RENAME TO FKE1A550BCA8FC7769; +ALTER INDEX FK38ECB8CF7F2C8017 RENAME TO FK71C2002B7F2C8017; +ALTER INDEX FK68AF8E122DBA5BA RENAME TO FKBD4FF53D22DBA5BA; + +-- +-- Record script finish +-- +delete from alf_applied_patch where id = 'patch.schemaUpdateScript-V1.4-1'; +insert into alf_applied_patch + (id, description, fixes_from_schema, fixes_to_schema, applied_to_schema, target_schema, applied_on_date, applied_to_server, was_executed, succeeded, report) + values + ( + 'patch.schemaUpdateScript-V1.4-1', 'Manually execute script upgrade V1.4 part 1', + 0, 19, -1, 20, sysdate, 'UNKOWN', 1, 1, 'Script completed' + ); diff --git a/config/alfresco/dbscripts/upgrade/1.4/org.hibernate.dialect.Oracle9Dialect/AlfrescoSchemaUpdate-1.4-2.sql b/config/alfresco/dbscripts/upgrade/1.4/org.hibernate.dialect.Oracle9Dialect/AlfrescoSchemaUpdate-1.4-2.sql new file mode 100644 index 0000000000..e5cfb444c1 --- /dev/null +++ b/config/alfresco/dbscripts/upgrade/1.4/org.hibernate.dialect.Oracle9Dialect/AlfrescoSchemaUpdate-1.4-2.sql @@ -0,0 +1,69 @@ +-- ------------------------------------------------------ +-- Alfresco Schema conversion V1.3 to V1.4 Part 2 (Oracle) +-- +-- Adds the alf_transaction and alf_server tables to keep track of the sources +-- of transactions. +-- +-- Author: Derek Hulley +-- ------------------------------------------------------ + +-- +-- Create server and transaction tables +-- + +create table alf_server +( + id number(19,0) not null, + ip_address varchar2(15 char) not null, + primary key (id), + unique (ip_address) +); +insert into alf_server (id, ip_address) values (0, '0.0.0.0'); + +create table alf_transaction +( + id number(19,0) not null, + server_id number(19,0), + change_txn_id varchar2(56 char) not null, + primary key (id) +); +create index CHANGE_TXN_ID on alf_transaction (change_txn_id); +alter table alf_transaction add constraint FKB8761A3A9AE340B7 foreign key (server_id) references alf_server; +create index FKB8761A3A9AE340B7 on alf_transaction (server_id); + +insert into alf_transaction + ( + id, server_id, change_txn_id + ) + select + hibernate_sequence.nextval, + (select max(id) from alf_server), + change_txn_id + from alf_node_status; + +-- Alter node status +alter table alf_node_status add + ( + transaction_id number(19,0) DEFAULT 0 NOT NULL + ); +-- Update FK column +update alf_node_status ns SET ns.transaction_id = + ( + select t.id from alf_transaction t + where t.change_txn_id = ns.change_txn_id and rownum = 1 + ); +alter table alf_node_status DROP COLUMN change_txn_id; +alter table alf_node_status ADD CONSTRAINT FK71C2002B9E57C13D FOREIGN KEY (transaction_id) REFERENCES alf_transaction (id); +create index FK71C2002B9E57C13D on alf_node_status (transaction_id); + +-- +-- Record script finish +-- +delete from alf_applied_patch where id = 'patch.schemaUpdateScript-V1.4-2'; +insert into alf_applied_patch + (id, description, fixes_from_schema, fixes_to_schema, applied_to_schema, target_schema, applied_on_date, applied_to_server, was_executed, succeeded, report) + values + ( + 'patch.schemaUpdateScript-V1.4-2', 'Manually execute script upgrade V1.4 part 2', + 0, 20, -1, 21, sysdate, 'UNKOWN', 1, 1, 'Script completed' + ); diff --git a/config/alfresco/desktop/Alfresco.exe b/config/alfresco/desktop/Alfresco.exe index 2bc8239ab159b0435d0c3b24b280cc28c48d50fe..1b2dfaf2058dbbbff957b5bd628e8ee0bc4a1e8a 100644 GIT binary patch literal 327680 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
  • ^4LA(rDI2*U~R9+YzJ-G6XgK5zv! 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
    g*#}t%xo=%8^JU`UoZ#`Y$}tDm z`qq2?AM)M?Jj&`!{GMbc$&e3bghV0+i4v6-v}izy6Eu7bAQ}wDBt!|iTglQ?TNj=| z+$AA#5}V1xRCY^uv864j?9vrmy0sENNeE5ChkOWPP}Hc@KI2f0nkGSG=KbC0nVAgH z_TTq^UGMeZ_kFz#&ok$_&-Z=qbD#U%AFj4liS3C~<_D8yw)?xr9`Ap{bzC1};Me6m zCt6>?w640Sc**TW#?Cy8#Ws!kclG;9dh}NPxF@CI)peJwM>=uus&TLo_X71u7z&X2 z0t5aF)I@s|V4n|=<0zSwAi zVZwUmD&sQb04y)xvN@NUaJZRDhRI|olR11y5dxH=WA>f@`xLeQ`dEsZaoaFO-3x~YrG=v2IZRO% zSwQ0}N=+fLLPxPwvmTD2q}C30X}IKJ0-6C$6q6z$o%#{uUd0p=?;uP(AG?6+m+R&X@D6&$=S((uo4`Mzv%q~SA-QQ?Mn1k@?E5Fhkw-o}_`A*Vti^+#pK z@W12FpGJ4~2jkKC5Vp;ezNE|;G*A1A>R%FED%tRmjB1;AU~t`4VwW=chJ#viQ_=z) zcAjgzdr0jIN5$YHT|X?_LX+ASoI&*NF$&;|+A`YtE?*z-KY!EMc3rsNLbnH2;2>nc zt=;Jzs3h>USQ20+;52Onj4RZTrB}3h2mTJ>_`qz<1Thu-BT&x)6`ZE>p8uf^#0if3 znh)y+RFl-^B-H*qg8{MASgy!^7S6?2qRt??CUF>Cml`LT0=Nj_Arv!Y{Aij&nQ(Zv z%(oIO#$3x=6+Ley48V81Rbjm4HcoqbP8ar^zTbGOV&IfBmK{=Mtm%*7J7DFU=OQeiPYJ zs&ZC_SjN!58M5k9X*^z>59$Ppv!OLph<}mZ^<#)JbYyj^G0H5-H!aBAC#_3f{{$oW z{emAo_|xLDm9@F=3Rg4Zdz91ZZJqD0Q~JVTz~kbJ>w`4-6>$(j6DbK*fnVx7Z&_+H<9g2`QVKcgP<&ChylW$mwqLbUs* z1&GJYM&s*Ul$AXR&o7l@(EKbk_4DS~s?+qY*?Cz_>poYVz1g?eImOmQkUX4pxl@Zx zhVR6E6r9A(Qu$;3=kT+-Qnb~nwaizVY3!Dd)M|fhWe*M@sr7PdEmqZ5bS>huMi%u@Ti`rq(&Svbr)eWsHuv^A^8@+p1zQzoB8 zi}Yd+b;C-W^T@5VNN;;+aj=*Pmyf34AoW=A*eT^G>rEVGeWHsZX{IQWrdy9GMN(1X z6h+e1HKItGj%%z;o~b;Yw>^{;_L6nCXm%=W&t@YtDr?1bByYlG*lgV5pmBr^oKjPK znu%GtNiCl&mXC^9K(HHSkQI(G-pE%Xe?s5>SY$l69x;tAXRV|?A;pDG78y^PiTA3+ zso}&&Nqk}&Kh(kOQMO2B>l3y{`RP=(?um&VPYi^GW)(%oon*pf?unV?)6!Q?i=@vX zz5I#kq;8XcIC*kcaev+n1-GKe_@R6c9i@$zV=TF(inDPy7LDSL25{s1=4XgOVjuoi zJh^@e;drgtrij&pJ<_CJ+p1A@g-(kl+Y-D%z1S?lYqm+psC6=SRgv28(xzo@lTPx% zLFkJj!)AWtOOyS$G!B zbV;b%OhVNjojG2!&AH7(fC=p*4)4i?tvxs#wl>qKk#fReYo9U0DS!^5qGp%4FX?3- zCxu~Gg4&<@T+xQvP7(}*ikN%T`OOGSQL{xeUL!AUxFmRhurt`9%Ih&0MU(B zd%+wUatZNsgBL}A+HVtSX1eDi$sFI9$x%#~JRZ!%Qmcv@mnDXl2G)i`p~g#$-*cXc zH2h)Kg-uMa4zIh!doI2P7kKcW+g>E5$?`V!@THkQj31Is?I>X*tLx1cbCMHJm<3)m zEMQsDJwBv{p{zuSpo&7ouw}ctS|cFkbho2P@{jK{gJs4-o{^tGGZrrSL^Kv&Y!QImAdwpqACMQf0{^hpZ ztE=C2580#*Fi}fV_1jbPme@TBN*vf$yyS(A5ZGW!_=Q5yhO;9X_I)?CWJ%G|asoyn zh~`L74Rro>6qLajuH{%GGE2mn1$@IMN6JFK^|5HTVyrF+K?cgVSRm3 zwm(P_rWh+Tx22esA_p1wK=GEPdM6BEwx#H2Tkf24Oq;mmXSl+9T)tKqoW$5|RkKMd zAIE8br-g(jYo*pSU)uZ&4`5hfNA5+{|QRlvx`Ho{(6lHnW><)qg@ z6U1KG_-7i!9dUY|Cri?@bT_gHGc5}^NBvJIh>t{!<7Jx^fii?JbR$`-(CdLlKrrD& zJlyWN9f1)hqgcsW3G^%`vmP;-<%bR9X2I=0a`TYq&Mw#NoY6IIWwK3UG+8Yshsh%w z_qttjb7q>=UO^2tb4+XD&uGZ1>-=-0rp`|Y2eS`?7h~%D*^SWc_VZ1h2QYQ^4CHwR zw5LnO0vHK_@Uv!Zq68o^RkIb+$#5$=@x4>ntF5n@9W_>tTQ$Q!J6e6t*Ho{4rFy+f zv|e0qYu8CHd#Qf74rY&mm9(>}RmvJTV;NJ;-dJp0)+$8WGhkfA3kNOCZ^gvio#jcW zieDNOOO|%5Sp2t+iql*DF}AgKXih|28n&(7z!rm5t+K6UHqM(&<6*`2qY%5w&o-$GfyKuC@OkFmz@DJI zn&x>h(-2t{%jn`Nn{gQw0~~%r75Ij4TArE*&saVsg+EEX+8*CDvt8|2p=-aH^3Jg@ z%DAem=rLvwg}bt@M6W)ZcfGdQpZ^O~1vew+qqcImKosu~!P$M&0uM73h}Pui*}4Aw z&3V^*dg{KQgEy_Ns9`%**(zS`!yMvbC~kn6<)u_|6rejP0>z|__#`3V^Y zTtlElwc)701qiiP28{ABNSU#MH-(vz!(97iYNA;){;UO+zFgdb&DzTT5=WdLcW-~! z#V!3uC$&k?fMpnTfJfto9X;56GXyl~lrY$N>fO=sVwgfyg`mY^L0ekt^PGj=5xI=f zM=P}s2r&iU{+OCPXzW?-0a@~PiC^Y1!qmEgq4UfXoU95JOPb2A&6liI(X2h%j7U}m zL<>>z8blB_7((P8?ScwndKQNUW-kBhu5alA| z2yGKX@(!NLMTo&mUPY$qA|y%eqM5@kLbAn0h*P-;NgZ|(k{NLk@)VhC^6Nr965wxo ztb35}v)PYy53&dO)%os0L{nY+j^B1uk?8as%u&NxCRH;0A}^vQ$i9eVi%2D+lXO>D zC*%I-Wnu=(gC4k-iY!JOvW(ZqNmudKCl*8tTzHDOL>y;0ah#D_ zL>0!D03h4l$7w@5jWVyfZSz%XWDCo9~U_HD00bcNpRnJ?E=hPGFrF!0B{8Bxa8X~9*tyyL~rk?j0|DvAv8b0-0Y1FCb zea1uT`GE0{>ZuvuQO~u;->K(1qnxKSK?Qm;ZjsbyLLS%F`-kjome4tx731ekv)$`D<{e?n+n}2Cp#-aJmv| z!zuTfDe10+`f$pfW=f_jp)s7Y&`im4C2R<%TxX_ayAn2rQ*u4Ek)_h9&%V!7RQQGIHUb39?oS^W{}Z~L+%uG5uO@84FQ9#REiK4up5 z3AZ+8NhTd0ri5~U=9pP6(wa|MBirgBc=`;X2&h|x=DIpW%rQ_@tVmH259vFGoo_q6LBQ**Mzs@(syDfd72uT&pCYrLcVJ%bY+2^ksg;i1z# zR@vJ9(%m1#CjP_NJO+@s?~|ROgS_BS#*0ihyVa;7W1OtK z;1Rvu`@zKT_DyabNYHH!612cSZBO2qjc>+G(C`zUqQrN+MkfuhCQD4^h7nQ`ew((m+O zCl}i168u?yX#{(nYL=ejc=pz7Y~FX{JXaV?%mS_-Q9yU5Dgf&?eQ)rtBI9<31`c^p zxAxJMIE`_RtQ#1jrSKH^YEYXHvP7srLpne7h8+GCsm1Rm^Mdn7On7{r&C1|SBZc$L zl}4>pIL)Z!A#8BuY~~1eh_l(vQr7RX`L|eq+xz~+@4h)14<`c&W&V6MS$dMSt!ZIS z{QjfoYR|>{sy8+kJ|%a_h*NdH)ZBjzJAKA3gF15isO%g|CAPB1$Enc^ykkj*b|btB#X1-&&h((Fji zcJ-3RxD6K|c3gO|j&p2ypSKXv1l?PM_vXiGw)}X{#VCpAzKWVS1G0Die6Rb-c#OzI zgRP`743Zb}>@-H95nJhU?<{vTY*M9cl~!mIs?wGgq3&)3kP3#sYvy+<($#?FVYKq_ zLoAlSxA`J?26iy*m&-5rmRwZAp2Dzcr=#It=u!mDcR3zeApnKDWDchkZt=UHaVbPx zeWhNqE9BnQG-h_~=gDG@u({omR>06UI_2NG>L)Fr;HqPK4|%5a$RKTEbluO49NGh5 z1B##Fp%tHi4D=ND7ME)GDsMngjMl|F8h&Vw_*tx3Xf7!$<&uijkBbw-V_(67oKnlr`aeLD7 zhy=mW3!ju#LGwV<2GR6f9v;1^ddZVCFwXO&^{{GVvTC2TJ|7oDLy{>xK?G&== zP;Oso>B^^=2fyD=!VB_*DvJ7SS7m0%QkU89X$CUOWfBg)} zg$>b0-4cV7+9<1MlWS=GiQp)6BLRc>$sOjC(b4z0w>lmcwt;wVm?3Uj5>oUpd4o(rQUdHe8;FTsN zDv;u~)Xp5Q&Q9ZvixhAc6K`C~t8bbpj#;O;H{X4i?ibQ5D#D*$C@R94{9=1^t{jl< ztsXVc@UC{T@d1Y;FT2`7p62dAG0OkJh)wM!0NB)?h{Bnh+O1<0RSoX{>zkUkr^tBX zqVu<_3&T7f+sse7>xZJ@8z*?7Q4p=dSg0!X7M>A}GnQz`z($Q#aK^CF3AIE0NlI-C zB{afXDz6U4>pxKRQpE0fS-@=K=2Uyf>`d{*5WIjNHvjB&K9B_BSa2cX{X3h*hr1cX z0kYMPHeN=mWex`7dnH?p?(SK}Ha>>;6={sI=mIqY%2{8h85)W0T~AfpyW`n;aW-#v z!Sc4|U7Q5kwiU4FH?)*irDJWQ0xkp-UG3FLdixXdMdYDN&E}Usk==#h@scWAsc(jS zGj2NXLqgSfTm|O7`ka&-OxPySzSMYcYqocx0fItyF>kkRszxbs%t?weffM?eWnQe z2#~*AVq5KSi8dDXh!USvpNC6~f~Z-<@8bL4jLx1Y+Z!7kYWCDsonGMNW^c>CbN$g7 z$4;NH0i0qZCak=9{*fC@lu%uti5oZ11+FjFV9#LZ-Z$RIWm0gx`NN8r5;}$$X3fRM zYU)E>OZkd}DVbna$6Y1{JDDQX`eT-?#-6;T>1O2tWU`jTQ)yF}SR(Nr>~bB*h0;HZR* z*X}x~z2u7vJ{LLS{gb0%Az#F-2nGr-rH)6w%~QJc9Yr}(1I7GCNe0dP*gvC5v9Iaj zk&p14;?I{k=$oHfP=*mL)Ei}KfkdXXM?#QkSC<;8Dq*Nxy9_5}yn~IU{w&2t8a=}6 zsrY49Yk7GvUhils$jMoskCy&yRoW2&pfo9{O}(21iMQC;g8uU_6cp!K7Q+2eg?;U4 zJrRe1qaDx2&GwDP0o%)M@NS3y1^gv4PslT>GL60QEK?4=0-hNQ+yU^m2(Wg%Qf~{i z09$HY9n-+eVj6hihz2HprGdiNXv<;iQ9!6lzz(>l$aq){Gwv514fin-ay{QuGhJFV zMo(u?P6JSwNiwmegy*-UV4FY-i=4D zh^)@o#>j?>U_B&mo~fw?P8%1RJymh%*XC#tpy2arEezWRykSnz*R2EPrCBsMY=@Or?kf5)>=US z(w*?hz^^IXl+mup4Dlnyv1Hj{`m8PI!j!XF?;*6D*Ig=WAbek2*;f zk|%N}jQ?x76J8LN(>z+lJdALE{Z95s`8I+9aeqCKhb!@?m2c~?SjMbP4sdTW80Htk zjj~;Z#m=uICxU>z#Mhde%!}bZX+v&|tSIGc+Hlub+Ms0YYvn$=M@fa0R9i0eE5aVN zG2hD)#>>YY&CRa%^;Z7_p;D#9Z#;im2)H;(oo2FLuO&%Bdhk*S3X|or6%|FiUYCLf zGK3j|$B1?i=RXeF5%JAlUPg*3K`eBd zrH@7&`s2ProN#pGGs01SH$~x$j4E&n&3P8&*C)sbXv?uZjPu|Vz+mkmRAdZqQAuvB z4N-l;9+$j$-VAxVjBWQaLOtbZe%IVpH`F7yDaAgCFMLU3t|fOev=)UGO6crKFr%KD)Q^GZp53UlMATp zij8bIgmS!^UtVNn@QUPb%{V==Hh<2_QOv@I< zwCu(aExY<_S|;*}jg*4&{#P2<%Z-{e&{2OEYr2szxzDx~8Q0HKd!m;675IH&c+VJI zE)249&z1yXR)Pz~Px_Z=w&PG&8MYOf=Qb(5;Yvz3>L*5az<`Lt6*rK|aP5xsw#LH+ zF<_6UQ=DQ|O{RKysVg=B*4nPT4ms+V$$}=BZk)%7aLgxrzz$>`Zl2y6d~s2j?Ne~Y z97SQC;5H(He!-xC`CM9YW!a1G&#ZNVoG^>L@7V} zno>ZO3-l8F$}ep!>237<8Qq@XM4_$2oR?kZQRunnL19ZuaC~XEcIJ&4Cdyi_#ECaSf9KocRn%d#=#Jni zHB~V-Z)$agPeM3c!Xpx1;c*F1G-5b7wPlNh23^Yu#sd?=whm$K$cy5oB9QQ(?yRMy>ue~}-&&&)JA^b&T1(~YU|mAYfW zf=awKOek>SKF)o5)$21W&I%i7nC8-_0|&`Ln(1?0FEqSj`1LG_-j{`oi}Tze9Nxh- z6Z8(@eG&=^ZuazXj7j5{hlr=g1`2#`r|^>O+D@oE|8hgJX7~UE$J65}#G+*8GAQMb z0#4c<1CRR1Fu15VYiW`p3s2kuISa`T$MAWn-b<^Dd`^Y5+C1t-`vW@`ov+LUnJ=@R zXQd~yoyrb$Vz@Sm_;6bLZ>iyFSKGQ@dIu67eA1sf(^cqvFemp_S6fX+)2$i3Hyp(D zzA<&C=A5&@RdRY=(Ca=C@2YmL+IHSxb_5@W{&P^pzQs6-=)OGg1Zy02VF{d@>gMpV z(2S`{&5j1NHbM&nYXObW<>(n2U2v^{Gj0K=*7!!0T*8NGG!$JOIH&DkzhleBBKDsZ z&!H^+t~P;<%!@%^OR$s?Z81{#5p&M|U1&&fjC1z&QuY;;b_oV!Ax_i-jhA3&_~v*y z9S0BV9Y~gn7{113)4~VXcs%pDIi+yupuy~IBa9W6|th3To;beasH5H8Gh_J=HMvv2-BOJ3G1)1!#ZaY2yTpkVHzret`d= zwq52~?Jz=YRE(H&sPZhe*-j>`k<~LrSn~^JRv3aQX9V-BtV@I6f+hE4otJ708f1v` ze8|7?p2M-gK(m=Taf2YeQ>n#d&w78T;Jz9j`bV2v^C?K z9H^BiI$qwtAZV?cR*s(eBG>Wt0V;MKZ_HdbruL>8>(X#7fl*`a%sj1s$WnXbHJ*gf zY+EqHUlUhm6y7QJ+-0SPR;q}8Sm|BX19$t5-E$I)_0G~V>?<#~)LJZil=0#bi6uYB zrjK##KBe3cUM_Z;xRdk@YGYvs?RBk|d=7P!_k`B-$Pu+9&luF=)!`ejslKH)UuN*u zy4EFV-uZ@a9KVcy}R821E>D2Uc{b6jfc| zJ)265Xnk<|_R8%>`=vtQkNW1~J-~g^XLal$uH{^t;5zHA)Y!FD1}aCGyojOWA3ir{ z5eb5Cb?S1xE%%kXj0-G+tB1-;OL9ZGe?stwy-Rh*0=F2n3>(6vV+bRD%CjQ&GnvER zviK&R&Ly;brWBAH2`%r$0>%t2FTTtrw0x!tEiV=`nPO=Jv~z^TOb<|5K+ns`jCwv# z$7n`E%R9}`@|jr6WU0{dD#(0c)NW=ty8LaPh%HUU^1H<|d+oV`)fu(tvR5ZMo<4f~ z!!~gOU;FOF;|Ax9+*gH8sdG{)DAFIA)Y;$KJ=il?s$HXq60t_L6e5`{LZP;Jey>uOjY7o z@ge0z3jP^N?LFEnQ+g=6PV>&v{#|rWCrml0Id9hNH+#kwSM>(%#Z`X}T8pdtLKzwdRXxd@nuuU1m|-kR zkK8#JxAW@DWV2@virz|$Gh8IgRZVsj5|Hrq#AT-P!Rql?_bG;U_zJ?>D$Vlb7+;K4 zm+ai`?Z*Xg;xA%uAeUOnY9%84aF`!o&9x5v7co zC<3a>0=edyKqK06-=rPX(y@}_)PQ`om;QNil26I|Sq7+~RN?1B+7qSzlAhby!h4IBgfiBuQneq-8IT=UF6j!t7=ng%BuF*zj+Ws2 zT-D8#IXHz3U!pFZ;qbmqde0bFS?}h2?wTccLe*t@f9nVF>hj0h4*NP!mk7*W&0#ux zm2)3n?%)YR>FXvdFx{4DUbb}85S0>|o@lJ*cA(t6L&{2A5d3*$rXtsEKc*pVG-BRA z$P}>M;)a%kg5FvCt`+~D=i)s#RsPZ*hhL-l_A>p(DY*KpnOa8ZrP>e3P-N7;FXSBS zRW#}6r`ESTXSyJzOG${jP|@pJ!C5k;{mP3ZL-^t5-bO ziiZsR*7na&wG=34DO7>)dqtx}&0B1)K;!h4OJxZEZ8AfM(^ewNpQZzMNe9rGK}E#V z?NS$O4!^M6s`Gx^E>1_w!-I%?#JJ@Tz?bK~ItQzt@c8Z07pC^LdbMxmmDoL4C?NU6 z_Y(ts2?HIxD6iV?N$+compPab{)7*A)NrZ?RmAxD!Fr52J>B8K-&JbNRF$+v27L_8 zk!Ut701aPRr;Ty+K}NrQ1vSabL@C!438#y?88W-G#?N4gWEbFy3ZV&iQq%B9FxB`3 z8m9z?VPgD~UbWt9s|S=rj$UCG5mt$2KdwA9B8sQSuft$IJ!d@*AkG#yT|&d*V!Y3( z^HR+?iP4B#F>#iw{i~cH&oZ;9eE&-)?dAiT+;hC$cDC+>rilxJ9kpLvj=*J^3`OI4Cx zk}l<&Q(lt215VFaz&)cClr4X$$F@MK_>D9NRAD>E#_3r?Yo{*mYqe=G90$FDX~FT8 z{jJtG?V6^8wSm8@J$Dhw1hWiedt3Lr3e#6#M2qct?!i@8dTg!(YcAy{zUuIAfW2Af zZ9D9qZ286fRIBh#!DTuO)~_qltvCQ5lzwm6VN-jW=VAmHH+s9{wNZ5#+i;uu;3&E{ zl>k6%t~82Sczy27xDLx@d?Nfg8vj9OlRGmfzE4aRx~!NQz^RGcH{LfBP@LWLR}7vv zeH>SUn8BDZ)6V-e>{bFPaYDWd+GDC#dT>gnXwfE@)|nz2EBlr99{-4TbsLzeOmr>Ve*T*KQv z!4s)LX$dS7va6C|eCmb@~@7iD-EKw+85_&*us~m+rzU$fk&{;}d)3+V75>O%uNqaBRPB{gg^8}iYsMSz zA!d%vtecsGH*qVFw-nMvQ$!nIII8=;Asm%6cpaBwlE$BXWgP1cw{42SPf-BboI9Mc zt%ZMb;EKmgGViA4G(F;GD~?&9bcJ$=YsF9aEU|Ioe4{6!lCZ)QXA7@V6%xzG<6<{6 zpxq&MGuKukC~AkB#Rz(?*v$-x$<6fw#Y(x3+2D-4m_0!*{;Drvv1xQ^&}d2md#Hh^ z?NW(nTbR#-Pz3t~;5z~k>`@x$a>dY(1nXDGxnoIa*~eYlnEj@&USYDTT*@kxZ_I!oOO5p`1K01%qw?7A%d!Z|W53ONY-5Y%+i+v!btQE7%q}(Z%u+<4 zdv#0!myIZ3%-0oAt6gfGwF?&zJBL3)J}nZW-`~Bw0xp|aR<5_SPhxG zQfC-~KY^>R6?P;kTP}h{@hg?9U=VPEQAFF!y;-s4#<{bqWrC4tfu$Ocaq%Z88(x%~ zhWH8IpNX&w5gzT6iAp6ZRoxVpn=DiWgU^E1vAzGXSx4X#8W+ak4|o*}7QdY_7%WCg z=VS23H)y~J3|4I{GTQMDK8(Tjk`+R^K7zEX=}81>|3ST*NP8FWv3PKcd>h7t|1G5D zUL&k|F?J-zGP2;SqP$b(e7JjIaT* z)a|VMBCdsj#Ivl=ZFsK4)S)GBSwpuct-w2AUDpeh-frU-ro~^~{nS%c@lP#PewMu} z?3Pl$r`unBV*B>(`iy+|{Ka^3(_8VZGEqV?Cg{iN-kS((fCQcU+J_Prc@K2Y@vm#> zu9}$JQa!&_$>aW3$d(~yIv%!mzrB4wk_4xw- zsyLTGtV-|>#jTF`Qd!7lh3^6mFAp6Pk1-JV&d~AhOOZYHN)xKns;uSG_9>mYEuTFl zPN=lFx!Y#aYrS(yn)KdvsyZz^N>2sSAU&=w$DJV8dnr~Oz4^xhwLTNZf(wuV=$|M=G@EIL~Z`IH0Z z(1H?F-7wZ9*?(oKD*xJ4*)6%RPB|Q%Du4ckuAx;n*XJ@1qPJ@C;YHRSoXC`25}ht~ ztWV9^vPwj$YxE-vA9@>?o8}a8n)tdY!UB^?nH|B$m=XU!GT>Dh&o2$!Nf%ucrVG}n z<~cTqyO;>p)D?E~tM-3Nled}eTu5vES#ijd13#sC+vIoM3K-VVdTZC;(AL9(&mY9# z^GRk{U+3@Ek4(Zdfnc*lS4yew^+{rN52$_oj}erv?p=5EZ$oJg$bznFT#vuc39kRt z#rq+EjA+U!!pza^l!|WexdKlbe6n70h9FSB3pi|@%l4oWkm|eW?uZQd{|E)Wg9iTp zkAfD_YoVah`~R0w(7!fe|KFgXOPQ4akreb+!J0H_-v4Y0YPRzp+8Ikh<@f(&3M%9J zH57Ci$$u*aT{kSLh8yLwfuk@+V%S2?M+vJR}(Ivxk?+;;j(b~eT5XKShXqX?UxjCEC-$2un% z{Oz=+_&MHlDUKg}3;rvVa%6$F zDqHYr0=iGmze>xwp-p=(f3D+)5=b%MUE|FEw&RD>RGK|aruo-0thEHra!eZfpheod8}5-BSay zk0O)f2j8QRDqG;^oaX$CBr;m0Cl4l7jSBpLFr3SINm4Q;`fyw%wjZGl;?e^H1^wY`-7-#Sjz zc3@Vs(TB)xyOj~nT$<;8W{s052np+mHqw6^W8-r_BfWM!@?Ss%3E5q~NbK?&UPN{n ze3|NV_XjVc_XK87cD&p}u+F1(=ZetwT0hDh*7t)6&n@A%uW(B+B|p#c=v{Qk{PaoS zHr_*p&rr{8wKrL`UcY-E^0a6C?(QPvmP^4aO!4+}w#+NsjQg1`t9w^o;Z~28AoY2L zPkO9)NSY;eIUfB8G@i99$oOrLSUxYW-N7BtNB6i|vEp*n|Bj^UwEnJ_yMBqJ$-R>= zc{qo&Rr(6|s^&~E;*@fT$QeW z=C5w%OcyGAUZ<@U&7?YZ7bok5hjUwqn1i2vG*?Amb+5Xl&uyJ_WYX)G%gU(4JLh^o zhMv1a4(}JBrGyz;@9_4h*U*=H96$Jj`MT$(2RSVH+=RW@7ce9Z7h)3^^t%r+V(UE_ z-s(;q6nU%n5l^`C(!#@m4q$Tk6Hv!<>4N^bNEd(_mv@WJGbS&^Gg>re3yG7nFYpWC zE-l>b_jGl*cO=04>4ls1k{#Cl4X^r2w$zmPZ%y0hZ6naM5F5R4t7)0%*geX-0t?Sy zA0P&{KP%bw28WVUawut}JT5L8ExzT0Xzqs+#az>S|JL|W;TB?LSk}LwS8SY*k?H18 z;SRyL^*^lZh;NY&VSV54K27#o&sDmoOD{ZeY{iMzz(oC|wJ+rp{9LzwIMKR4<$$S^ z0KDj&6`o#yfYZAGW)Lp;G!8(QyOu@Ce-MLfw>sRL5H{dXA-Attq!iD~@a66&8C10d9lXed5c~oaf!sd5x<19dq0;B5 z^esrnFwys5yss>g1BB@fzVmIlgv6c1J2&IQVG(-c{JqFu+%0o0dHfafw}ZbQ@i*qy zxt1jUp5Y?Lm{6Mpy{G?7?jnh!@7ldkeA3fCWKE~gx z{2k}-1b?sbcap!?`Rn5E6n}4g(e8fpCUz9*j<0YNv6<$^Uxi2Xx$)UVl9I@q)jgi8 z8~Qxo#6|WMm6y7R5rcFMC+=ys?8dqA?Q^Yh?Q?C@+UF*4fGh~?$2YjS+!}sPkiejAXB-0h@r!7hNpI^y_i-c1i!iR)?wI#Uxl?qVTp_o$flu=-3Rnj-`Vdh zL;@AGV-#;>`fYQ++fwMSx2OK;#j~vggtzK+>^Vi~tK637gxvMN=?&cDBAQOghd$2- zzUucX(fIZiZg$lFN|C?kg!m~S5)r-B|2xT;0^gw@ByLvg7uJUTj{1j$;O$Sf7H+Pr z%Y)t(Zdo-}|HNOtZ~Jy=Gb9gT{1&~_vHPK9y@Vhv`jMAqa&r$%Iyq^`=WZ5PugbNe zDOjs?o%GuX{@CL1S}D2GHRSMq&V*LZ5>ltA-Yh7cQz)H*bdweb#0)>TkDQ(f1^sSA z5wfv)o->}&d7f_8oX_)N;8iKZHRQ?BOSTmB&xugGRYL8qh1z)xpXUsBy@6i>=;z?< zdD2(?Vu!m$5xkNobx(`+)dp^ZchxNQ-;qIhs2!B#-D`(of*9%;e($~xgJgX@*Lx?u zD>hpE)5p2`p?RMQ&D%(+kb{k=vApPeP?5bPuX_h%Zzp7Lmynh9%?lP__pM^hwOFWp zePe=2^;(4L4G8u@^>8y^-3u-sThSxLE|l^)E2s6NiPl$BPA15G5l;7TenL=Vp?WRr zZ;vEaFVv6rzv0*o(SFgow-4Q5p?l4;?#u%39}sHyu@uhT;Q zYLQ9#dW7=zD9TsTBS8V6e4j@s-=~W5eM(okC4utw3FZ4dO!*nZx^Md8t#I z>xbqmC}#)Z0v{`oqn-)us}V$V3!=FdqPaf|h}lvlD?S~FE`^H8dF~UqyLTVeUJ@&B z6g;A6wKE2-ZfAp4m<8sQFfE{5vx!sQ6)&=np41;;AHC4^HaPW$;M9xM#6Z7j@hw#d z#nNmBp`MH))C&bT64)6=sP+G1Zlbs@bPd$xM-b|2L8yutgnBdOjbVfe@0c#}!z>sD zw=l5&wrFbgJiVj8i^Xakh~N{eg6O5HJQJUSQeFg~7OHZ3JjcWqX@}ZK|1vsVd% zSOTV;^SQyb>JNk63fE%sj67yha`~>8z}`Gl=}XqGb?n(6SD2pXPG5gzrS~#H@=cc7 z_BqzL;r(PzeES^sk~s;Mz^}1?sLCjZgRHUo3JHN?w0`8Qve$hybQ8z|mfs~Y<*LsR zaOBvE?p8yVHLh*I=!w>rl-65BErpAOHyZk?ukC-k?gLi2rDie!;XL;nt=+P6$d07+NK{y4RxvpI*?=;=j}8tAJ=ewh+Az%yv=^w zqs7f844^-Kn(HJq2f(AN<7Qh64~3aD|MZ*0agOIj2DvqmY3-|Rorb2DF)^j5F=Z}H zzIk{$)9wz!uiwI2x~0i_Oa6@N(RyJ^a9*t|&oipsI_(x$Ma$|>`A2teU2U6N8L|vn zzu8qAB#P$2#--MFEbA@Aq>XYgi6@1FsdY{BB-{c++UGhY8DFXNW=E#6K&El;(1F_b z(rWi+Hx|XW#}NaAdHiF)kea?WrxsVx*Jjs<(`M^yv&qxm*M`Q0x6|3z23_S^mq5-I zZZvp}*AG5#;Vd-iKDXs&av1t-yE|{7|2J{KiEbwdkK|w>S zMN0)3Xis8FdoVs_uM}$|^b}BBu%p7%x7Vp9G}khveS0Oq&{K3y*7cOtiHYR z@cTYM7 z&8J<<@6s+*Z}D0(2MI@-FmP?&&VshI0`+#*$%Sv(Pfx8w4VKH(+*j53R5|Z7$L#>) zCgXU3adgTvzOOA?o^djsiFF60m5$xSjM^h(G^yhyX{I@h*|Vf`+SpmrGxRbsO-p$# zn4EGlDES!RNv~+B^kTG>5@%7V430!Rbectj7+Q`f^yZ+pdvwy?NrzKfU3+nskB{U3 zqI(3$N|O`z?iO*Nf9k#*r*yZfRbt&?8F{_)CBXm&@|!dzn_;Z^@`_0fS3&$emLCl!!qnI$jnb7 z`elKZbyW*{v50*N6g2aopg|$JNfnlO{q;%fa&r1#opdtgbvQU&Tm+sX-4L7gB8Gjc zQGf|Ux%6c+>e7>QUp4!3=N_rEp@TLbh;-*bq&o*9-8m5VvNWloJ%USmC!M)0+?mXn z&dBIXXA)ChXPuq=y7q%cs{Qhm_RBL~-wXa^=|^*qrL^|%W#HkG#G`qwC=7_+ifQ>V zZ2wVOjOtFxeytE4YP7<3&;IS5&t{9?zAKW?G$mh{u{crQO`jYL#yKZPwB09Ex&s|^Z<`dR{mc}6AD(|#~~t= zQI87e6B?q%wumMxHkVr-pUa{_#X_@U1H0`3KFcM1GzCo!rM`)30D6~7zvb{BhtAUu zc<@kE17Qw#_g?M#Qp0`{wPDuHawe_R_~nbDbJ>8VA|rvXV->UQ0%B#Lc$iEiyfZ_n zEYxKAYS4fXEoRgowu<_LUF>pj=K^0xEC6a3C5R4`Aa0=vrV@lwQ!siDMw_6#J!q(! z;Q!`~kwOV5gVw(wN)gqA=z}m&vFZ@Q*9Tvt_)_CH0wjKKTBPm`qFiNqa~o^C`GM3Y zsuSFd281Sv`9##s%Xs7YD1Z{H8s9b>6g+A=y2(G^dz%CAZKG{?Z`0a~jOSmK7U3rV z4`PXEgkK}NRMRC9-QX7H4E4X1h-V=&1+F+F+Rzsn<@4_n$$pa1xNiks&^f6)PKU0GrFtOa}w8%zUnR((`fx>o3Aj7RcWw1Y@ai9F zuQwIN)xWA8ZA!VpzRGsPfi=G=D3I7dcy%0=*iOu7bcAijL&^*vw@xPfer9 zXusir;}H!UG&8@8N!biBIu~1QXEFXHa5Q-m7vt~vDzH%pc|#3v?veeirXcF#K7JMc zdM{?9dvL6=7j<2IA5D&Na6blNzGyy=>ae`^7DLyNaGj?-K=w@@+L9yr%c2gy` z<6;F!rQ*@xUl}P;T~sz~XQHja(E6Vv?i6sbACEf)v;GT>Ia2>1LXbN4a*Ka&e$A4M}L3G3W2WSTQuu;{~(B@_;a{ z*zUY3dee42F&sz$ub%3MO}X^68$aIzcn`Mvc;|bb9mM6y(!{EWnRFs2K8*%omeC?W zUD)Pve5~IDlnIatz!?Sb3jr7q0F%Ae!1%I186G@+Lg* z=whP~_%dUGct>SCm0JWCbW&n=$X5fARA|yCR&i>X@c7sOS@7`W96N zJ&7{tYww<)xyH;KyhSXM7h)Fc$q+qHe4{9R(9l6?wKI5A=m9L5dnB1ZQzaDp8m#R2 zEZU$K8~;qnCOwaB$zM)N(nv$VGCev_2V@t}PrDfH}L>%an5Ch^{ zASU8KLKOM5t21bIMTHV()sC{Mg}#Tqs%V0{BCqI@NPm}0ca?J0}kK7Iw78s ziYf56FVoDgVha2={7Dtq=EKXl07*;FYR@1xnj*gA2aRttqhhjLzV7zCO9&`1{bih6 zFE~Afo!5X0C@|I7oeGfMvU(w z)k_(swYp~z-#W&rY-qAX6G$SX>O{G=3@fD{qv@NQUD=dH1S`L!(Xn16tj^ zjhO`^q>>QGOV}^NF83W?R`k5S5-KFg**Wgkrr|dEKCjk1!}za5YMo6AopDax;6SF$D;$ z_b%WOR_zpM<9V|a!q()HE5`3_ao7o#Znd^IEwqWh)N#fi2q8=<2YHzMP+)=V#INId z$5H<=`xK@uj)o6-LGMR4=nMIb&#d^CqXUiZAubaaI_l-fxX?33-`jt52j(1~@zeX> zkdIsE!d@M3&AtBo(S1ba)z&oKhSSz$?R&UgDsgK6fOn~s0<7<5mJD?uxFlcuywM5C z+G0JW>DJMg3+P+{yfFeu$oM!Q*^mYD@?;3CKwcq`7YSr~1QICNeu26I2PNVX2HIPs zTQPSEWgJp$fup_)5P~H%a9Q{YudZsR;|KdB%^FwTxZ+0KYqQ z{utrFH0(^-ciHmxGKWk z3GbWzAF|N6O$OcuJoZ05uxgHn7qC8p8M@UR=>ktetsN_&ra0Qp3Uv3%Y)cOtq=&-K zbn?8d$x*+5ZQ_Ce!P|vZbUeEsbAtif`mt2LYV>W6XB`E#&#mR$W6_MJagJvXI9~S2 zPp(bVjQ4P;WiH#`Kh1{Av{?s&1$v70aNlhDl(PVCsBuZcx=~Hz^pwUsMqf^X)f4%3 z#eyjV!7=?^k5$Fp4pU(_=5JR$gYlhG-Fr;A7lQNYCBVGtchqzwMi)HcU=T&d4`@KS zx9}_!p;rRn;n8>`?o_yOM+7$zo>mVeYrm$zG!|1+;eeoryqx3ZavkYi z;uXokZwR_1nXq1kFT^Lc8$C~`Q8m+(R2d7RI5S>wCR0!GYol#_iGnkuoU5W`KhF4&i#3zL8K3e>44k#Vcn9uxG^~`D6=yAqUWW~)I5c+4 zp|K3}nDC+TV&gfvoP#$1JX)4e*`Em-vV^0D$4iZkQo~GRO%%>N)J}ZKBm{qhv(z>F z7V>;d8K!>x`vC%4wi&dpxcV2uQdlhwFY>`sU^I~S~d69s_$|Rwz z!)+`sfc=@~d%)&rW|Q~ z&spa-dXVWW2Mc)**tOSQu+&1xFteJbPt(`h_%1hl`eADyqw1T61wu!}@C$X>K zjyS|vL--x(E0`LmT}S{S7jRW^Ftpe5!bVrFWtAFN@GjC25mv$GhxZusNLgz9paqVt zJ?gKY=wUWxcV6C(pL;}<93wegm9zN7cL%l{tk!H9giOYU zSL=HKuv$B#aOP_LM?w>0A@sMnHtwXsv8xqVYIfr|=pllspu@O7TJ+Z~&zZDZEze73 zRLlWZMmgFgSYwTsxHHG$4C*RV`@fKTigI5fL1Zx!LzIWe+Fp|TC)ZobEA*_=vSOn< zgVF0Da{Ze}7>xnfmkhZI#JY?RVScwn{xg2GTlG-yFozL4sOSmQDdvc`a2#YfMKb1@ zZ0w*zvB#Vx%_?{7`HpKi4VJc7`~cj*>9EN75yOrDwAqelkM+&Fc7jbCU(n>d;kb6A zWA}c?v#++kYfm|lSD5NaSs0wab#7iss+LxOM^CJBxRX5S>EJ2fM@u#1B5objFYHYQ zOylLKabYUQ$5aD`O8OZqp!LrQ-PPuQGKbb!{P*U-HZ zrexx);Oyl{{eEqUndk|obd~9Ct?xTaQ@ZrzJ)xnY8iKv|=N`27SkHMs33+=$xkqx_ z#qILDmbkb&PuAt~(2mOkW3oE2tg7|w<9sI3AmlWMt{xV@k#*HWl{P@8|AX%lK{|dVBi|*jSG?+3WXvK1QR4Fi#5s z#19JoV%o)9!7rXybjP|>kfrYIW;!>#mG~Q0GWbOu>LC(`P)0UO-22j6lH)z2y)WIz z3jsj2xt7%l&0ALEFJWa7REJQPo&J-$*nw1K%A1A%lL@v@R?I_ih56$#lO&I{FrX+<(dii*9|S}(pPXBdr5&dY#`hKB zz=F8DsKebIgC*5H@dP!hK9jI&d@$Wx-4o|QW_-r>pxu)!?ByC8+2iO=$9$(;4R*{= zZ;oTCN11I*3b#=&)J#nMvyivnuO+HwXvPlY%=#y#Tmmpoi?f#po?M+Y8R#3%t{OB! zvX?ULb_RGgIn0Kdt!l1iS^|u$Wy$SDEB6WJkLSmo<)QA- z6{|@S>4RR1<3XFgJk@%%Os`3|4p=*Vs0frgtzFimT<9q81Gri3%-t^20e*KXC(0!&%PjYY7XspsUFwix=rhr8 zpVCIe(AMsFDjGvTMO@Px6Z?;PyE*e6@;@988hNaa-FxvVwJo!YSh{bzTD1%OB`@B- zBy?0<>6*SEIlMPKPBRn?q++zc@I}110D>1156865P(~KSMg5!|xpz6eEuA8x;MOqs zrLj%t7*`W*JHqOK2sSw1878&juImaMELyZHtL#+;p$t2fP~b(dY08E^K(vkpgx+SI(|S=yEQf_!U7-OM64*TdtXgn0|?IQ3dP9Z{&e zSFFLZ;u_xc+!FE}rO>s~Ee68l`bxLV3T)vDE>_(_XiH?uxZ;)ThkJ!;EM9aoBZD`_ zyW%vM%>X7SpN?G=AUAmXp(O#X{vd2s7USiYzYG~y?E5kln6T$d2?WMXqA~xXFX20k z{@JW7jhoZ8HhDz;jB7QyLD2HmVKzxW651$|2~xg^(Eo6#s0dPfVcz^~;*l@T*5g7& z_n7G`ahRY0?%?)u_kBay?&2XvoMOfNCwsVmPO84>Om3&~B@3qcJ{Do%ce_|R@*gMO zz_MS#%_z75eNiuPuL)d>!1*I^ks<|tY81X#U(^Hqc7dPwP2ej=!n5`RlYc%KF8@V+ zQ8Vx!fqy{Yulg!{PhiKE2z@^_%DAQZg;djOdJP*C5Cqs1ZSVU?Ys`BO2$FS zxG$WMIW^&nzYhNNe*?d43h|Qm-fEb-j9RmRD^!^*h6_ z%hhX+0382!DM7t<4!^#{t7`GX*#PNHS^UcY-9bC>BcLym@a7AoQMJ;j5Cb3Sc=+ir zv+^nnZsXrG|(9xU+_Za;}5?90TvDL_9k!5GpCaCM`licVQpYD z@czQ)z((~v5onaBe;I@`FyR^bv?bt?A{&;H1d&1x4FY?&%g-Ty;fX*QU)%hCu{jC2 zwh5%T1}ZWp4h_QAOVKk_elvBHN{zsL@udlKB5*+r%pc5De85uV+JBSwsMq_4U;lpi zwQTtHR$k2kFCeSll+VBP`+e{*I^ZYy(Y#Xz;fGI;9B_Z(lYxa>rPI#@dSrI#-rAqa zb8DdI$L7=EX`m#6

    107*cJG`s&?90^e5!H3$AdJ+}nz4nNDn&)ayK9oVQZ+CT;OQ9-m}cS~*Q z)2MA+q&Da?;e?0l>!ChdVCnCv&n)MxzQ~}Q9Lf5CWIg>~=S`QQ?_HZC)rRTApFzCl zEdu`Z2yg|zBMRT6FFH%{zZdvUfv+A3FXOg03g4|SIt~1f1-@6{3rE7scrOHA@)bn$ zoz@q1k*`AXO}>VF8IgRl-(*JN!u!(Aks(GvpLWip@DNY>5Dc}f~=k>8g}uc`Mzr@ zVIw8T_-__?k&NdA{E^p!z{8Q(djb!duXU}BP=&yERw+1p*sV4$X zDb-y++>#abL%P}j719#3M%hpFrtlb~s2amN&k*?AoHLyopZ$->FsS~wO8?ac@wB{x zD%;_X@;T%VAI)2?1OC5&7c9&f9Rn{^?}&hO3dj$_Aghu`fb0~IUIDpR zK)yn&?Sa$&$ixT}F2$~uJlWTiXPRp1*+4Te(UuAwECEDs$~0TLP~d9?-lE`p0*%1S zUSF%8Mqus8{28(N?UH|=es)Iun>Jz=X#*9fj0rv!vxO3)D=3 zTBD%OdRuLQk1maYlD%lNKy4PNBB1<*lLK8F2C4UuF|F6ckS7Cgl~YS(Znl!?+ho!g z-N9SAWIrL1Oc#)s z01>~8tv{pw z1nA?(W2$N9QB%e<3NqEvte;+ekTL3u+5;YG9$ZukE@8Z+j@~J4FPyeQJcLx?N zh{?SZkQV^an^H~8{FT736ZlF6e>SkY{7Y)>3jAW5JnI7&QoHYJWdd@$ zs$fgtlE-5}gbqCjh{;;qFTjlge7OQX6L{JuOS?1h=<=AVpCQv60qqgc&tc!AtrFb2 zQa&xbiBIpyCt8&ae-H(cKG_6hP(WUc%`GD+p$eMsDB#n!*iW*O56h>W^68P-Pu+a_ zt$ec0=F@liv@(4^f@&~&s2*&ML074}Q9!2&Xn~aSf3f#I@KF`#{{N6fr4((T)Qc8% zRj{cdMn$C>bvFqJ8X!UxY*S50HY9bEteXvjqDG;LikfOvw5U|jQj1>nQX4I`RIyS^ zEn2RnmR7XXCM|8Lr4}u%zxU^yGug>ulX&m_zV7|KzTZ3W*_mgaKl9ArGiUZ}j`k6G z-A~UAlB2q>nH?l2bf=WG(?+^w(oIM=&-Fmgoh;TyvAQ4f40K&aL8Q(>j#7fb#N zzBh}$pWnUXrjSF|NNTO5PP0-Yx}Ob8&63o1Nqr(C)q#}m^_PaFKP&0{5~SaH&~JyU ze;De0&7RI8i9CnMZMSXF&U4I?2^da4}@5s5UX?|te3*9J&dN#fWvCmr(Qqc0*j*b7S(_T6u_@+cAOQgmV>FgfnifH9tB9XL2 zqMpe1?i*fmth6B#m&gi<9N~#9?Y{qcM`Q^i#S+;pkq_?or{)vqIwESdqa?CdB0md6 zhWtg{y|a6fE4l^IJ&drV&Ly1;N^m3bk)0A58CcouipZC6MO_I>a=?MLEnXK+y^!<_D!)B7SQ*8%Q5lNLwY8z6w zO)cs^+;g|1d$<=zx`%l2*6tiH&hP$!?Ylgz zhaR`Q`yi@u+ij)FdfLm{*!_eTuj_u)i%s`~Uc9sW9xpEJ-r6P=J-Xe!h%D>*Eu+c7 zdCeYw-MNHRXT7R1%&SIc_e{^k>h231z4I5NG)|&p%MpF&UbRbC_pAHV+Fe0wS5c-% ztykN}c*CtN*84W(PYkopdZ~Y1>b5O6VR@@q8^l`bS>E0KiFJ;)I^{`;?29KwB6mrJ=V0u+$hn=g@~RLi2lG6&F_BB6_wR5NVGztpH#v7-B9l8=FcrSwxu{v6;5f{VyIjq}mi)yMpd|O89mLh+u z?PXwfrX{#GBe)=o;7=sDRf0E3kc-1vpL1GyGli~^h?#{*t?Q5a=qq9+FM&11)85v7 zG~1b;Zd`*%ljq475jhSK|5Wfx=9$&sK~HW zZ&^|=d7=I>rGMe4zooRU441s{%o-%S4;cE759>E5_ZeT}Wu+hY(_5APnxFoa(s%pm zE~T}v!g~1&dc0@JN`A3A@ z9k7g)o^N<~If<34^s!bS>-5p7kM;W4ppT9E*u+QIN)~C_YKziCDQwZfyGg2U2QHUY zG5j+5MM6+!9x0Xi@E({e_vvFWSu2O?BVQjQ^f6K&qx2Esqw8~iNd-zj%wZ%&> zwrO5^v%P!wJNtt9&nt47RfAK9d@Vs--nH*deXY+s0$(~n4F2qsS-Frz-AeIa*u3cM zT4KqU6PI^w&d7`5$ybr6TPZ&85?ZmS)GS+DqGaddQnSuaZuxSl*`nk#n-J?>P3u0h zQK<)&+Nsow+$%k^L#bPo*GA8Fw<)FB**$hTDb1hmW0caU?jEUBf#eP$#cC7`26++m z?hEyUIXdY&_#rj=z|91-f*5s0B&T z2HL@6U?X@Pbb$}RkfuB{3XBCsUF!3dBC-m6C+yaJvBPk?*C5|9LQ!1>^GFd7U8IpE#7dFBo9GFS&5 z2DgLxpbDH1ioh}8P>=`q)X~0RGk6-T03F~)kOG&2GEfA@fe085a=?4>JhKbD0XBo@ zz!Ts>a64EC>Oci31*d@Hz#95SVOy;qbIjegYLIE;@z^&0AW~aC7y@|OxQ%Cl+W3WS z8 zQjAsb8rT5V;m>2l_k(tDEl7Y$a6T9h3cx5Z6nt<6V->s(Hi1sC0^9$f8C2r`^Rfr;9B@1s2@V8% zoAC>51Dn88paa|pt^!S<3QPm%fN@|H7z*|=zq-I1;0NGo@EEus+zze&b>cRQobTAtH3K+1P@v?__DE1;?xbg|F2jfWT-}40e!OKd_ zVc?hdmzZwwA@~$GHc@y%kvE0XF7<`QW=? z6L<~SFm-&fIc{08Y5hjAIciXenQ&N%xo%0ZWt;Xd;tP2;tZ;ZX?}Agt@K7^btO6^*GSCJRpc1UeDK%C3QFHufi_L4-Ma@p~hUqSlFzGEH6G;DJ zX;fK#hDdIyx$?_Vv*MFcQ&<``SKJ;o-@GGg?z|ypc0Cg1nPnfhlGl#Ro5c5lpMyuh zL*SJAqn@nfh5U1(=E9qz<}dd|P2PP`lU^J(?~tCqC~E!+es*uv7;t-(4RyYl;8;;ix&C__&9nCWXC!1-rm!ZjG7|#8r8vCUO+AheypCCp|A- zlWtBGRh$z^H#bKT%}sM@58AoG>!n%~p{JVpOpw=Vn&_2!s2cJb`BzPjWD>|X(O*OO zH^!Xo#S`hnl-H9H@=h^FBYCvfw%P2XkGL@LV2i zTx;aZySkpXIT^XW+Qiqb!wbzCwWOp6mTyMB7LAmnqq!FGdM)ksqxx(vIahj~T<*1z zX!V6HS039^OQ=H=|C+s?aHXtw{#XgOpdl z!&>SbBP|_Yww72{B0V0pFhZ03^IO?!Yt88GLCb%_mi6pEr<>^w=~n`UzS{G@+KP>| zgIYI2OK43^(ZiZE8hILrefPNU*MF^@{T@_3&kSnX+n(lD8V^(Sn?DmYyIc5I&1lT5 zA*N>~vGdwpQBvM&)Z~pIjoE5vY^WCgSl5W~SFAAKt`SKzQ$RkbeKguNDkG%g<_h%v zkt&<2hptNU)gQwu-yfTkNcl6=&JWFRt(Y3Q)}Q@LlO?+ngnd*U)Qjpdt$SK!wQ}q~ zze>y$R*;#2Ej!ax?nE=+^ErtnJ99LrYVb|#v%I%!sN`pOKKE|Vh&fj*%Fvpe@uS+C zZStyy+>CwIGR<{DVW4aCK>Tr)EzPZ|ns{Yvx*<_nTAi*gZmp|}r^eMJld@mxl~v7} z-%2~yQ6Kf79rtzK3UVcK5pX6SpH;fRpMz(T9)q63SWp>^*O^%HuWM7xNoG2_{?#ds zZOvp|o$R$uy5V-|ORI&Jnu}$%j#^){QJ)%94c<)k$4abh(u|2SOQy?q$@D~g;{2L; z(%(LrrTAKb%Ij#4E2*`{XC*sr&2f$XmVj4jW|5~^(u`aSG9})a zmb`Xq7Zaz*_t%_CT2M1bR@9=B*UFUi=BIR0_@edBUw`~`DY^c<@Y{Wcr>R*eJLz7s z)8M7O-IjJJnp0`k2JIsJxoh*(Mt(W|ypyk`lv{)3WKUjK-xNDGUyi#7=oT#MUgN)` zUHqEZJ(=})qSvb056AIeb zTiUbPy4Y*wRHU`~PB_TFuiAU#!1q_rU zZ%PKEGIRCNYNGL&U>4|pxP^973niibF|M71e+OJfzOJs?TPU0tPtRP?l8!f)#_OtE z6X}bllvOv)ZLOXQ8{S1N@l+NLFZ)2>-*F|p-phC?KU*BHS?5a{Yb&Z#bK_~RNY;3J zwee|vx_LvNE-~qw_^Jn%V0lS+3YK0H&|YTnv5dR} zKk#-fO)q#He?x^YDzI~u`0bL(H%J|h;!UXR5?YY1ioDJw);6?H#QI>lyu>bt$tP-Cl zuZ)_=uVQ8q@e!+{<|*QW!KLQUYdBk&Q*4r?zdJ5w4*o{eeC6?|Ss(EBKo9;{sX2ta zCB&aPBWAX8R&f&f?>_0DTl`sP7hjK>v%#dNqNWz!SwBY(`MaN{+##jr4dMjx-fu@u zC-E82a>ns-Kh~<;$jDAJvbCDHUl<~QW4f1*RFw?nr>-wp5A-}U)-{4i>Sl37jkz8^FWV_X&IbKPrVhEFhs%o>%f z`vT=_ebg%8?-BjAFeBy5Ol2Htr*7{!!u$+pIx-h?J<{$YBW25)?yU#COUy|5Wl5$s z@K(DSts~=U=~F$Q+Ze<7&(LO|f*g(Q$9B+e&e&!>ncCdc!1>UWrn=^s))$AKWhOSY zHkLNDBooyOO5^hyYT_-u@&efw6C@_3nj6cT8=BH`z{#7z5tK_a^J<&5XQw=Wt(zK8 zry6Qnq<0ZlVeMG#UZr%(%<{6>CEnGh#@naq3aNfDL0$=WBDy!L^?FEmWZ8JKtt(ay zS6BaT(9-^?qwWJUGXAQq9Z>_Tw!QPo@MqB4{)+9}oQaHf{jQD4PzkhcIhL+KH;LUE zr@RZ0UErM`=?Ys*8`LAEvn1WeW$L4oQ{{L~71T$&Tlu27=I_QbeySdI$h9Jo?sdMB zk#Y5{ulA~xv6yK)?V0EH)Yi|}&MM6~)3wg){=hfwy!!V+er#uce*C9VGZTCjbb}X} zpF6v& z!MQ2;AvpFI+&3LuYOW!E5gfcdYQ6xz0Y0G21iT}E%-Qc>GVA@{UdnzB92pYpwcdXZ z{cPvJI@`VYRmuXV{gZ3NZ=&XnLt^Gm%*6suOnWL@>VX^ zDlikI!QJ3#@H4O%9QLEAITKt4+CV#40lp9328UqlRB!>P4{$Sa^~Yjn^PZ@w`D@fn z#|LliWc@4$Gof!fEM|_zzuQ0LdI7CE9Ey(4xOM&dBgmtj?Adr5?RG8lc70X6K8Y(ZANDv^_{{xRMOno*xXdo+>}l= zCm7=U=S`fi@iIBxn3x<-BxjMXudhj`662a{;4B@+U&whsY8t?0;Pt;n%^4rW%zW}& zpf3P1u=(An%$SZ(l$xQ?Z|$Wn;C%Y~a`0*JYh=#-W7IT(2f#1E5r2xBFMczeJz`K=}&@JcCtV8hhh)+NK##Y zz@MHRKOy;E_G+UHH?K>*o67>h0H} z=Gxxb-gw(dxpvZ6_Q)663p3Bpwi!XJ@p%yK+*k1Ty8doA#g+Clu6f!4&gINSClu^{ z_?+vhmy|YCCz|J)g*;K4Cdb}$>|&p0!wS}`PS)D|;W2MbzI9m4^mYCD=ZOD1uRpt~ ze_z+1gQ&B6{rM!a5l~gi7~`7N3Jw5!4vm>dKN<6`M~mS<0CWwT$SSD)ih{1+BVY8} z-k#@aItdfN50JO{8t#JfpCdn>WxPFzZ*^JnA3;7pG*30IURECyyFvM@$^V3#Nm}E4 z%O_%?+zdIDnH2H%j@l9G9J`dyVBF1^+a$@7NnBnPpI6&3ea@G(H8&;Y)$H0!y++V| zF}rh*C@D!a!~;eNIVC!M=(#wcHzV#J-aC1df6({&?}UDMckMW>2+Un);h_elQ`r)l(-rmK1=grvD zUdlbg9z49%+z0Q`EWAp155c?aL|^Yq$HsUaUCdky7J((;8E`fHZ-c*)J`VYdz%|Fk z%(LKcVEkvG!Lwiyyi3N!Oe44k+y|Zm?}6dSo(!si!fMKY1o?mb50$@{^1n&>hjCV+ zcKh$H|Kv}WnkOm$wEuzfOM*!_(D!6ZIq%S((T+*4x zW5Ce!7^CpkPK=qi!9UPB8~S|G*MKdPV&+)l+lb$VHy^v#|2bw}K+n#To1pbw&qvr> zwxnt*wa2WZCN<5exc61zCTxD};+Uxf?O@KRn0aJ%)SNggW=4D}JMTSsC9zaZeZ#z7 z-@y(2QSWwfzu%gCC}VYY%&fU2Z3ZF;b%l&k=`_p&GzhOwRxgT6` zaIq=)7w)Bg6XV%Pzl^mZdg`Q-sS^r=Z=a@CH*r8+J0s2)xA7*~Tl>{=a|uNB94il`-!*tDg~P^Z)!h`gtxgjWP2z;&+id^oud`^_rMDiTEAj zS;Q%D%B753+U`Q)$G~}{UnKr3dZvMAy*?c?_Yzm&XI@pzyaoT$#K(g{_^f+Mg;9qw zr_fsme<J|SUGGjKju7*cX72cnIi0wTIC{*w!;?#28P;($_waL`o3)#I|EMiy z9>1P>@KVfN@OaF8_~V!v^(=d(Gh2p^I{uYmkN*bQ(ksutC`2y}(FcU+35W9wn9-TD z+zrqRu8o=LchZK$PXM*$UBq+9Q(G!;@~2D9;_I0E_tD414+7iXE6G!PTiGqhZiN5T z{UDH&&6DW67s-o^DmA|%-#+&@9;)M-< z#9wCigQW|z$j?5q)NG=yo}k^HCjLIL>R_K$e4D&ojVIeBNl%xg9an0Ai#E z-|b^O@$<{rlib68f;PSr`h4hjpy%C-&!m5jj$(!MQU1I?{nd{?@9p!IkEXKE`!(dh zek1#un`0(=2kp5aW>$i|VB}TIF>nF61C(Bk4e+^z_ym3e#$CgD0Io;(koK54m-MyZ zHIVle`i8vcH?kLhF=oC1edR6Kya~OfG4tCNzg}MD%;>0|sR%BwX7;N*mkYyQuLr-J z*+Xum9R|G>Gxb2%zY~9z{knR>4t#l&F$DiA@CrBzdJOqO1n7R|J@N`k4}yOPI3K(E zUg%U%GPOEco=#OX^C3TX;nyg~FZY^1B2sFu`!(Ztr|(w@I%N$l+#wiKz%TOp(yKyG z*NW6TzMjqvW^$gbJ6}Edt1yo5MWz8iJ()Y>@*rKp_wrM{G>@t@)Ggo!Wa2z-JNOgI zSkSkc;qQ;UJ&beDu>s#kV8OPvd%e~@(JV@s$iFh~%xCa@ZW;fSe;zs|#S3`6B9*FM zV9ajnvG#AY_xmyPhrKaVbabgX_k+ybT~7XQz!LJbo@l+${L%WOw4i+D>A4<#cT%0M zsdrrU>S;f;CR=FzwXk{FV(EtSt)6%aVg5&<`_uD`X8DlaY@x6Gux#y5OH72!Z2gvZ zjQ`!`3xkTyji4O*WB-VmNjb$md&X}-Yu~j3p7ujq*}G^y{YUmgeLc6m8(Z#vXr25a z?;-F6IQHYk=6UeBp~dDpFoP2XJ&9<~79!raA@i955o4dCp3XNsUt&b-n;N!AJvy$e zzTrtRXY_57f_;|4eru&O?{J>vbkfwsQvkaE&pchA699cDlldk~))ib|`jX0SQ_mbW zQj<7$7QIW8-3+fbkZ?=tU9Pq7`*S7!Yqn={RcCzzWq&uoy;3k!IWIo8)LiwcVpDWz zvH2cxgj3lFhIAg*^}4^-&Xi#%97K94^4ZQ+?kB&mu$cWY#D^{m6fO+&+Hx}&fA8~-`0CAb*~Qvqf5p%HChg2bUj_a9u)|daNJ>Ngys%)I+L`k?P{{cD}^>L1_#y4TaXzAu%~_UR#gi<=7C#Q*NxufhI)`cLld z-)>`R$FbfMg8k{JPmg2uNK#%C@_p;{-RJw;=ggKn;fsPXQo-Iq_A5Pa#_&`_wl?fb zNBuU2@4vF?^!_e-iZ{}9EULB|%jX%Sv+3#iNUfq8ky?}dZ{Y1S3j5bbb@#`Ebh7tM zwsQO@6??Ck8r^nG_s!qm2bHh6vj2?RU$%F<=*dm(+%#wH69r@VwrZ@2&_@0V$7I%t z1ZW3ie_mpCfyf)2Re(0I4n%(8uXEXCKdM|dn@g~}4r~qVSKz}AP>K&p&wAparI=^(pml3?(<8Dw}502=lzm%CQtyzgJtB;CYIc;Ku)nMH`$R( zvmSqrwOD)2;?|Z0!B85e-8VvjrQ03 z{=SELi0kK@T#xMi+W#%jLHwWi9_n1$&wdY;rk%9c(0$vdyJ+nKV~$jO1LsMKxsf?S zaZG<@g>#nA6q~igXYkub{l4*{rpuf3m3-X!%|J~{;|{hWJt3D=eP86y%y`;=BywiDy5{opTB}plP3d^tTpmv~#S^EV z;wfM|)!LM9XpBeVsZ?_+V$8wZ@KuyepE=Xd5ou;lnRcE-n$qc0V^gNNIqZK=FFbYp z!9!wAk?LeJ(NN?4gfvoL-4bc6u8l{k$xrhnY%;aLAnOQgmBHAEyGuWpRgQ2~Aj5yu9}M$dzB zUInOw{5$pFAty#+88=#6;*qH{r<73uF4pnL8$~p3nI0xsfMPxktR&d zi~CXyJln#zz71D-=7&^Bo^RPy;W(O+IjwW`h{Xba2;;WIxX6@r1kuLkmUJZ1aCsc- zYJgO{g$IKp&2^C)etE0HXes%0Voh^nl0)&@NWn=piPUE!b*)YOT%fth_p>_VRFi#} zBSQVXN|YuNQ~4IqtGnubit63eoQ}ksnp@}AN4S%%p}vUgx4wy94d%Aj$)Wb@se9b- zycrdKd#CGhx|y@$aUZ=sRE~TrR2d~$l7E>3Y6Jc$6U1+As-AA&R6`#U8&-{+#qO^=?kan=}tvnrVkx?*Yz?Y|uvW`AOytBStOb&QAO|*=wIx zN(^Kg8Fmdk62wE(8U>e!<@;`zK&h6Wcj3apewRDb(wp?D_!X@@WrQ;tvt5{Ptt8uG;WjKd95;WaJtGDKlQ*-i)R7<*b-j(wgTy^!rYp%V{dk(08U9g^>O$5))6*RRb z5}#FVxxd$cF1-0B_dss;^YbQ7oOpo3X@&mjWHOA2taa|pcyfA8dRp^5I#C8HK5tC?gu+7q4#G3J;o3pv>?};%haa97 zo6-_&37S)r_|+H-Q#scb@94E>Tu6(~4g~#&L)ad8zg8q)PlWj2VCxa`-~y@w;FVRk zq`jZ3G2D`^NiWZ*eKTe%yAi%OEN-69vkWsQ&ak5}!h@j6M4b8Vn~s#}&r39?K7-vz zNrFplWO75COMgxMf=Fq+r6$#o4A>@8%&*}BA@58BB{wf4&gOz2ibg6HB;z3?Ro*2o z(xPhCuo!s@9>2j!)qciQHTvImM*Kgl!1|h8;4QMj(~&Guc;O9?Yuhg+S#k;dWz{SL!Zy6sL(eTRTC$hs@B>xJ1{0o6AL-K{aUBTOt$vUT4|@P zvo@c@*X2CRNW(X~rKt6Oun{z2V|7yf9vX+)Ojwz$4yf)u<2{=;{1%q+>XhH)y2ND+ z`TfR9tKYL_Z}E(MugkLO`y;lO)he38>je4NYt9CfRrzV=cXOTZ!#r)X=(Blqyxv!4 zRWK5izLW<)7lPwJQPJ7voO8}LV@RI_P6Q`_O zh{pguYkT53$8%EnsH15kB6**cbDpSbKY-LBX)`_&$qWVNz=+gulG zc#iclEnKw)%2O!#BWF5b1Q-nV{h`F{20Ouaumx-czkY(>uy@1jA|I>=D-^?92MT^$ zV%8F`0jt3(umUUx?O-Wb0v3Zdun^1#X^;d7P!DQB6{rNW!5+%3AT9@GU@|BLQBVXX zfbpOZj0FW?G>CvvU?dm{4A{GiGZ?THYy#^*2S|eoFdmEmyMK=iSPR-gJtzZ3U;-!v z5wP!f_y@YcHn1Kn2We0SMuWYy*>=zgIzT-r0ui7#{eL|~O!!{`$0z>3Fee*5nVnjH z{}!A5|Mix?-emp_G9T@qYvBftdF;O&o+bV$|EzxG|Ml*t{`>c8{eSle7<7Hlzd;`{ z*dA1T!=TK+oIxM$pVi;L_W#kGfo6b88o@tXqNq$d-+aUh2V!CQk5=1^RKCfi{nVgA zngngv->_)$mv6jj$<4Riy7ac&?`Z$Zop*KIeb2qi?z{i1%O7~~p%q_yc;%``9{u|2 zZ#?$RHIF~>uWO%t>fhFV>*;TIKJ)B%)_?c8?``=0^Dk_C@dqz$`r*qz+Wg~JezN7& z*M7S7-(UaPwx7T8i|xPs)tfusdi&QqfAh|7ySjh(`(1zd?rf|)w?q}tk(ND#`W@>K%hNPzQc@5qP0mpQK|&4EF)vN=28sNEOfD4|tP zeQ~Pb+2DXdkBl3R8yd7h4R&SIU`uvD6M73|L;BLy zIfmgxqcv5lr?0S=YVcB=b);H?JdQq^;sL=)h?n9}#N%>4;>Vndpli@G$n$qvz_zv<+pcfppeFamwi|C~8*#t^oYZ{z%U?dgn8L!g^0pINqR(2%?d@$>(1;ZOcK=O@FycO%a}=T1I-#pC1d96kOg z!=6sko@K9ZYkPd`a+ltGab(nn8Z;ob}n<9q&DtUVV7}t*Ap+NblMIB~E!Vcbif-0_>kzx%|auMGR{8Rf<1`}{5})Sov$bE+m? zzn6sQ4QH-@^s!&OGVFn0!@r>O!bKte9U*#ghz>tTy)g8=Z}_?EYeM|RA^O@79e&>U zfe^nVL|^g9y&r2DzGc`$x6uA8YL2` tPo?+?*m4bk_7=<7ms__=Y`!(Pvwe=I~V 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 literal 53248 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" +// For error or control response then return a string :- +// Error - "1," +// FileNotFound - "2," +// AccessDenied - "3," +// BadParameter - "4, +// NotWorkingCopy - "5," +// NoSuchAction - "6, +// LaunchURL - "7," +// CommandLine - "8," + +var response = "7," + runAction(); +response; diff --git a/config/alfresco/desktop/urlLink.js b/config/alfresco/desktop/urlLink.js new file mode 100644 index 0000000000..f498fb1549 --- /dev/null +++ b/config/alfresco/desktop/urlLink.js @@ -0,0 +1,29 @@ +// Main action + +function runAction() +{ + out.println("URL link to " + deskParams.getFolder()); + + var urlStr = webURL + "navigate/browse/workspace/SpacesStore/" + deskParams.getFolderNode().getId() + + "?ticket=" + deskParams.getTicket(); + out.println( " url=" + urlStr); + + return urlStr; +} + +// Run the action +// +// Response :- +// Success - no return or return 0, or "0," +// For error or control response then return a string :- +// Error - "1," +// FileNotFound - "2," +// AccessDenied - "3," +// BadParameter - "4, +// NotWorkingCopy - "5," +// NoSuchAction - "6, +// LaunchURL - "7," +// CommandLine - "8," + +var response = "7," + runAction(); +response; diff --git a/config/alfresco/extension/index-recovery-context.xml.sample b/config/alfresco/extension/index-tracking-context.xml.sample similarity index 53% rename from config/alfresco/extension/index-recovery-context.xml.sample rename to config/alfresco/extension/index-tracking-context.xml.sample index 415831e277..5c1343bec2 100644 --- a/config/alfresco/extension/index-recovery-context.xml.sample +++ b/config/alfresco/extension/index-tracking-context.xml.sample @@ -3,6 +3,39 @@ + + + + + + org.alfresco.repo.node.index.IndexRecoveryJob + + + + + + + + + + + + + + + 0,30 * * * * ? + + + + + + true + + + __AlfrescoClient.url - http://localhost:8080/alfresco/ + http://${localname}:8080/alfresco/ - - - - - - + + + + + + + + + diff --git a/config/alfresco/import-export-context.xml b/config/alfresco/import-export-context.xml index 392be9325f..a171b3642e 100644 --- a/config/alfresco/import-export-context.xml +++ b/config/alfresco/import-export-context.xml @@ -171,8 +171,8 @@ - - + + diff --git a/config/alfresco/messages/copy-service.properties b/config/alfresco/messages/copy-service.properties new file mode 100644 index 0000000000..ce041e4bae --- /dev/null +++ b/config/alfresco/messages/copy-service.properties @@ -0,0 +1,3 @@ +# copy service externalised display strings + +copy_service.copy_of_label=Copy of {0} \ No newline at end of file diff --git a/config/alfresco/messages/patch-service.properties b/config/alfresco/messages/patch-service.properties index c2a5bf70ef..76e1aeaa53 100644 --- a/config/alfresco/messages/patch-service.properties +++ b/config/alfresco/messages/patch-service.properties @@ -27,8 +27,8 @@ patch.savedSearchesPermission.description=Sets required permissions on 'Saved Se patch.savedSearchesPermission.result.applied=Granted CONTRIBUTOR role to EVERYONE on ''Saved Searches'' folder: {0}. patch.savedSearchesPermission.err.not_found='Saved Searches' folder could not be found. -patch.updatePermissionData.description=Update permission entries from 'folder' to 'cmobject'. -patch.updatePermissionData.upgrade=Please follow an upgrade path via server version 1.2.1 +patch.updatePermissionData.description=Update permissions from 'folder' to 'cmobject' [JIRA: AR-344]. +patch.updatePermissionData.result=Changed {0} 'folder' access control entries to 'cmobject'. patch.authoritiesFolder.description=Ensures the existence of the user authorities folder [JIRA: AR-497]. @@ -39,7 +39,7 @@ patch.fixNodeSerializableValues.description=Ensure that property values are not patch.fixNodeSerializableValues.result=Fixed {0} node property serialized values patch.updateGuestPermission.description=Rename guest permission from 'Guest' to 'Consumer' -patch.updateGuestPermission.upgrade=Please follow an upgrade path via server version 1.2.1 +patch.updateGuestPermission.result=Changed {0} 'Guest' access control entries to 'Consumer'. patch.categoryRootPermission.description=Sets required permissions on 'Category Root' folder. patch.categoryRootPermission.result=Granted CONSUMER role to GUEST on ''Category Root'' folder: {0}. @@ -52,7 +52,7 @@ patch.spacesRootPermission.description=Change Spaces store root permission from patch.spacesRootPermission.result=Updated Spaces store root permission from 'Consumer' to 'Read' patch.contentPermission.description=Update permission entries from 'cm:content' to 'sys:base'. -patch.contentPermission.upgrade=Please follow an upgrade path via server version 1.2.1 +patch.contentPermission.result=Changed {0} 'cm:content' access control entries to 'sys:base'. patch.forumsIcons.description=Updates forums icon references patch.forumsIcons.result=Updated {0} icon references diff --git a/config/alfresco/messages/schema-update.properties b/config/alfresco/messages/schema-update.properties index 5cebba18a2..f927e17562 100644 --- a/config/alfresco/messages/schema-update.properties +++ b/config/alfresco/messages/schema-update.properties @@ -1,6 +1,7 @@ # Schema update messages schema.update.msg.executing_script=Executing database script: {0} +schema.update.err.statement_failed=Statement execution failed:\n SQL: {0}\n Error: {1}\n File: {2}\n Line: {3} schema.update.err.update_failed=Schema auto-update failed schema.update.err.validation_failed=Schema validation failed schema.update.err.update_script_not_run=The following schema upgrade script needs to be executed manually: {0} diff --git a/config/alfresco/messages/workflow-interpreter-help.properties b/config/alfresco/messages/workflow-interpreter-help.properties new file mode 100644 index 0000000000..398b1b772b --- /dev/null +++ b/config/alfresco/messages/workflow-interpreter-help.properties @@ -0,0 +1 @@ +workflow_console.help=alfresco/messages/workflow-interpreter-help.txt diff --git a/config/alfresco/messages/workflow-interpreter-help.txt b/config/alfresco/messages/workflow-interpreter-help.txt new file mode 100644 index 0000000000..d8ed5405e6 --- /dev/null +++ b/config/alfresco/messages/workflow-interpreter-help.txt @@ -0,0 +1,174 @@ +## +## Meta commands +## + +ok> help + + List this help. + +ok> r + + Repeat last command. + +ok> user [] + + Switch to specified . If is omitted, the currently + selected user is shown. + +ok> use + + Show current workflow context. + +## +## Workflow Definition Commands +## + +ok> deploy + + Deploy workflow definition to Alfresco server. + + class path to workflow definition. + +ok> redeploy + + Redeploy the last workflow definition. + +ok> show definitions + + List all deployed workflow definitions. + +ok> use definition [] + + Switch to use the workflow definition identified by . If + is ommited, the currently selected workflow definition + is shown. + +## +## Variable Commands +## + +ok> var + + Show all defined variables. + +ok> var [*]= + + Define or update a variable. + + variable name + [*] if specified, define a collection + variable value (comma-seperate to specify a list of values) + + e.g. + + set bpm:assignee*=admin,fred + set wf:notifyMe=true + +ok> var [*] person + + Define or update a (cm:person) node ref variable. + + variable name + [*] if specified, define a collection + variable value (comma-seperate to specify a list of values) + + e.g. + + set bpm:assignee* person admin,fred + +ok> var = + + Delete an existing variable. + + variable name + +## +## Workflow Commands +## + +ok> start []]* + + Start a new workflow using the currently selected workflow definition. Start + Task parameters are provided as name/value pairs or references to pre-defined + variables. + + e.g. + + start bpm:assignee=david wf:predefined + +ok> show workflows + + Display the list of active workflows for the currently selected workflow + definition. + +ok> use workflow + + Use the specified . + +ok> show paths [] + + Display the workflow paths for the specified . If + is omitted, the paths for the currently started workflow are shown. + +ok> show transitions [] + + Display all available transitions for the specified . If + is omitted, the transitions for the currently started workflow + are shown. + +ok> signal [] + + Signal transition on specified . If is omitted, the + default transition is taken. + +ok> desc workflow + + Describe the specified . + +ok> end workflow + + End (cancel) the specified . + +## +## Task Commands +## + +ok> show my tasks + + List tasks assigned to the currently selected user. + +ok> show my completed + + List tasks completed by the currently selected user. + +ok> show tasks [] + + List tasks associated with the specified workflow . If is + omitted, the tasks associated with the currently selected workflow path are + shown. + +ok> desc task + + Describe the task identified by . + +ok> update task []]* + + Update the state of the specified . Task properties are provided as + name/value pairs or references to pre-defined variables. + + variable name + [*] if specified, define a collection + variable value (comma-seperate to specify a list of values) + + e.g. + + update task jbpm$122 bpm:assignee=fred wf:notifyMe=false + +ok> end task [] + + End the task identified by . If is omitted, the + default transition is taken. + +## +## end +## diff --git a/config/alfresco/model/bpmModel.xml b/config/alfresco/model/bpmModel.xml index 09d9b4498a..d88eaf5a7e 100644 --- a/config/alfresco/model/bpmModel.xml +++ b/config/alfresco/model/bpmModel.xml @@ -187,13 +187,14 @@ d:text - + + d:text - workflow_item_read_actions + read_package_item_actions @@ -247,6 +248,29 @@ + + + + add_package_item_actions + + + + edit_and_remove_package_item_actions + + + + + + + + + + + + + + + @@ -262,24 +286,29 @@ + + + + + - + + - - workflow_collection_actions - + + + false + false + + + cm:person + true + true + + - - workflow_item_collection_actions - - - - - - - - - + + diff --git a/config/alfresco/network-protocol-context.xml b/config/alfresco/network-protocol-context.xml index 10fa179cfe..f5d0408e00 100644 --- a/config/alfresco/network-protocol-context.xml +++ b/config/alfresco/network-protocol-context.xml @@ -57,6 +57,7 @@ + diff --git a/config/alfresco/public-services-context.xml b/config/alfresco/public-services-context.xml index d93bb54e85..364fd29a21 100644 --- a/config/alfresco/public-services-context.xml +++ b/config/alfresco/public-services-context.xml @@ -80,6 +80,9 @@ + + + @@ -124,6 +128,9 @@ + + + diff --git a/config/alfresco/public-services-security-context.xml b/config/alfresco/public-services-security-context.xml index fcf2b92d31..1ed94603a2 100644 --- a/config/alfresco/public-services-security-context.xml +++ b/config/alfresco/public-services-security-context.xml @@ -325,7 +325,7 @@ org.alfresco.service.cmr.repository.NodeService.getStores=AFTER_ACL_NODE.sys:base.Read org.alfresco.service.cmr.repository.NodeService.createStore=ACL_METHOD.ROLE_ADMINISTRATOR - org.alfresco.service.cmr.repository.NodeService.exists=ACL_NODE.0.sys:base.Read + org.alfresco.service.cmr.repository.NodeService.exists=ACL_ALLOW org.alfresco.service.cmr.repository.NodeService.getNodeStatus=ACL_NODE.0.sys:base.Read org.alfresco.service.cmr.repository.NodeService.getRootNode=ACL_NODE.0.sys:base.Read org.alfresco.service.cmr.repository.NodeService.createNode=ACL_NODE.0.sys:base.CreateChildren @@ -374,7 +374,7 @@ org.alfresco.service.cmr.model.FileFolderService.listFolders=ACL_NODE.0.sys:base.ReadChildren,AFTER_ACL_NODE.sys:base.Read org.alfresco.service.cmr.model.FileFolderService.search=ACL_NODE.0.sys:base.ReadChildren,AFTER_ACL_NODE.sys:base.Read org.alfresco.service.cmr.model.FileFolderService.searchSimple=ACL_NODE.0.sys:base.ReadChildren,AFTER_ACL_NODE.sys:base.Read - org.alfresco.service.cmr.model.FileFolderService.rename=ACL_PARENT.0.sys:base.CreateChildren,AFTER_ACL_NODE.sys:base.WriteProperties + org.alfresco.service.cmr.model.FileFolderService.rename=AFTER_ACL_NODE.sys:base.WriteProperties org.alfresco.service.cmr.model.FileFolderService.move=ACL_NODE.0.sys:base.DeleteNode,ACL_NODE.1.sys:base.CreateChildren org.alfresco.service.cmr.model.FileFolderService.copy=ACL_NODE.0.sys:base.Read,ACL_NODE.1.sys:base.CreateChildren org.alfresco.service.cmr.model.FileFolderService.create=ACL_NODE.0.sys:base.CreateChildren diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties index 49b3e270fd..7f121adff3 100644 --- a/config/alfresco/repository.properties +++ b/config/alfresco/repository.properties @@ -65,7 +65,7 @@ db.username=alfresco db.password=alfresco db.pool.initial=10 db.pool.max=20 -db.pool.maxIdleTime=120 +db.pool.maxIdleTime=120000 # Email configuration diff --git a/config/alfresco/rule-services-context.xml b/config/alfresco/rule-services-context.xml index 79955d57a9..d5697b927f 100644 --- a/config/alfresco/rule-services-context.xml +++ b/config/alfresco/rule-services-context.xml @@ -21,6 +21,9 @@ + + + false diff --git a/config/alfresco/templates/content/examples/show_audit.ftl b/config/alfresco/templates/content/examples/show_audit.ftl new file mode 100644 index 0000000000..d439d7c9ee --- /dev/null +++ b/config/alfresco/templates/content/examples/show_audit.ftl @@ -0,0 +1,173 @@ + <#-- Shows some general audit info about the current document --> + <#if document?exists> +

    Current Docuement Audit Info

    + Name: ${document.name}
    + + + + + + + + + + + + + + + + + + + <#list document.auditTrail as t> + + + + <#if t.auditService?exists> + + <#else> + + + <#if t.auditMethod?exists> + + <#else> + + + + <#if t.fail?exists> + + <#else> + + + <#if t.message?exists> + + <#else> + + + <#if t.methodArgumentsAsStrings[0]?exists> + + <#else> + + + <#if t.methodArgumentsAsStrings[1]?exists> + + <#else> + + + <#if t.methodArgumentsAsStrings[2]?exists> + + <#else> + + + <#if t.methodArgumentsAsStrings[3]?exists> + + <#else> + + + <#if t.methodArgumentsAsStrings[4]?exists> + + <#else> + + + <#if t.returnObjectAsString?exists> + + <#else> + + + <#if t.throwableAsString?exists> + + <#else> + + + + + +
    User NameApplicationServiceMethodTimestampFailedMessageArg 1Arg 2Arg 3Arg 4Arg 5ReturnThowableTX
    ${t.userIdentifier}${t.auditApplication}${t.auditService} ${t.auditMethod} ${t.date}${t.fail?string("FAILED", "OK")} ${t.message} ${t.methodArgumentsAsStrings[0]} ${t.methodArgumentsAsStrings[1]} ${t.methodArgumentsAsStrings[2]} ${t.methodArgumentsAsStrings[3]} ${t.methodArgumentsAsStrings[4]} ${t.returnObjectAsString} ${t.throwableAsString} ${t.txId}
    + <#elseif space?exists> +

    Current Space Audit Info:

    + Name: ${space.name}
    + + + + + + + + + + + + + + + + + + + + <#list space.auditTrail as t> + + + + <#if t.auditService?exists> + + <#else> + + + <#if t.auditMethod?exists> + + <#else> + + + + <#if t.fail?exists> + + <#else> + + + <#if t.message?exists> + + <#else> + + + <#if t.methodArgumentsAsStrings[0]?exists> + + <#else> + + + <#if t.methodArgumentsAsStrings[1]?exists> + + <#else> + + + <#if t.methodArgumentsAsStrings[2]?exists> + + <#else> + + + <#if t.methodArgumentsAsStrings[3]?exists> + + <#else> + + + <#if t.methodArgumentsAsStrings[4]?exists> + + <#else> + + + <#if t.returnObjectAsString?exists> + + <#else> + + + <#if t.throwableAsString?exists> + + <#else> + + + + + +
    User NameApplicationServiceMethodTimestampFailedMessageArg 1Arg 2Arg 3Arg 4Arg 5ReturnThowableTX
    ${t.userIdentifier}${t.auditApplication}${t.auditService} ${t.auditMethod} ${t.date}${t.fail?string("FAILED", "OK")} ${t.message} ${t.methodArgumentsAsStrings[0]} ${t.methodArgumentsAsStrings[1]} ${t.methodArgumentsAsStrings[2]} ${t.methodArgumentsAsStrings[3]} ${t.methodArgumentsAsStrings[4]} ${t.returnObjectAsString} ${t.throwableAsString} ${t.txId}
    + \ No newline at end of file diff --git a/config/alfresco/templates/content_template_examples.xml b/config/alfresco/templates/content_template_examples.xml index 85ac61c026..ed43602646 100644 --- a/config/alfresco/templates/content_template_examples.xml +++ b/config/alfresco/templates/content_template_examples.xml @@ -140,4 +140,18 @@ + + + + + + + true + Displays the audit trail for an object. + contentUrl=classpath:alfresco/templates/content/examples/show_audit.ftl|mimetype=text/plain|size=6134|encoding=UTF-8 + show_audit.ftl + show_audit.ftl + + + \ No newline at end of file diff --git a/config/alfresco/workflow-context.xml b/config/alfresco/workflow-context.xml index 468f5d350a..ae0f4a3ece 100644 --- a/config/alfresco/workflow-context.xml +++ b/config/alfresco/workflow-context.xml @@ -23,26 +23,33 @@ + + + + + + + + + + + alfresco.messages.workflow-interpreter-help + + + + - - true - - - - - - - - - - + true + + + @@ -73,7 +80,7 @@ - + diff --git a/config/alfresco/workflow/workflowModel.xml b/config/alfresco/workflow/workflowModel.xml index 73fdd9e553..30dbf7b625 100644 --- a/config/alfresco/workflow/workflowModel.xml +++ b/config/alfresco/workflow/workflowModel.xml @@ -19,19 +19,18 @@ bpm:startTask + + bpm:assignee + bpm:workflowTask - - - workflow_item_edit_actions + edit_package_item_actions - - @@ -39,21 +38,29 @@ - bpm:startTask - d:boolean false - + + bpm:assignee + bpm:workflowTask + + + add_package_item_actions + + + edit_package_item_actions + + diff --git a/source/cpp/CAlfrescoApp/source/alfresco/Alfresco.cpp b/source/cpp/CAlfrescoApp/source/alfresco/Alfresco.cpp index 31f08bcf3e..b35d4e6922 100644 --- a/source/cpp/CAlfrescoApp/source/alfresco/Alfresco.cpp +++ b/source/cpp/CAlfrescoApp/source/alfresco/Alfresco.cpp @@ -309,7 +309,7 @@ DesktopResponse AlfrescoInterface::runAction(AlfrescoActionInfo& action, Desktop // Build the run action I/O control request DataBuffer reqbuf( 1024); - DataBuffer respbuf( 256); + DataBuffer respbuf( 4096); reqbuf.putFixedString( IOSignature, IOSignatureLen); reqbuf.putString( action.getName()); diff --git a/source/java/org/alfresco/filesys/CIFSServer.java b/source/java/org/alfresco/filesys/CIFSServer.java index b62f92fff4..34fa3fe64a 100644 --- a/source/java/org/alfresco/filesys/CIFSServer.java +++ b/source/java/org/alfresco/filesys/CIFSServer.java @@ -26,12 +26,11 @@ import org.alfresco.filesys.netbios.server.NetBIOSNameServer; import org.alfresco.filesys.server.NetworkServer; import org.alfresco.filesys.server.config.ServerConfiguration; import org.alfresco.filesys.smb.server.SMBServer; +import org.alfresco.util.AbstractLifecycleBean; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEvent; -import org.springframework.context.ApplicationListener; -import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.support.ClassPathXmlApplicationContext; /** @@ -41,7 +40,7 @@ import org.springframework.context.support.ClassPathXmlApplicationContext; * * @author GKSpencer */ -public class CIFSServer implements ApplicationListener +public class CIFSServer extends AbstractLifecycleBean { private static final Log logger = LogFactory.getLog("org.alfresco.smb.server"); @@ -81,29 +80,6 @@ public class CIFSServer implements ApplicationListener return (filesysConfig != null && filesysConfig.isSMBServerEnabled()); } - /* - * (non-Javadoc) - * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent) - */ - public void onApplicationEvent(ApplicationEvent event) - { - if (event instanceof ContextRefreshedEvent) - { - try - { - startServer(); - } - catch (SocketException e) - { - throw new AlfrescoRuntimeException("Failed to start CIFS server", e); - } - catch (IOException e) - { - throw new AlfrescoRuntimeException("Failed to start CIFS server", e); - } - } - } - /** * Start the CIFS server components * @@ -264,5 +240,27 @@ public class CIFSServer implements ApplicationListener System.exit(1); } + @Override + protected void onBootstrap(ApplicationEvent event) + { + try + { + startServer(); + } + catch (SocketException e) + { + throw new AlfrescoRuntimeException("Failed to start CIFS server", e); + } + catch (IOException e) + { + throw new AlfrescoRuntimeException("Failed to start CIFS server", e); + } + } + + @Override + protected void onShutdown(ApplicationEvent event) + { + stopServer(); + } } diff --git a/source/java/org/alfresco/filesys/FTPServer.java b/source/java/org/alfresco/filesys/FTPServer.java index f77cb0b67d..270b209373 100644 --- a/source/java/org/alfresco/filesys/FTPServer.java +++ b/source/java/org/alfresco/filesys/FTPServer.java @@ -24,11 +24,11 @@ import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.filesys.ftp.FTPNetworkServer; import org.alfresco.filesys.server.NetworkServer; import org.alfresco.filesys.server.config.ServerConfiguration; +import org.alfresco.util.AbstractLifecycleBean; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEvent; -import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.support.ClassPathXmlApplicationContext; @@ -39,7 +39,7 @@ import org.springframework.context.support.ClassPathXmlApplicationContext; * * @author GKSpencer */ -public class FTPServer implements ApplicationListener +public class FTPServer extends AbstractLifecycleBean { private static final Log logger = LogFactory.getLog("org.alfresco.ftp.server"); @@ -79,29 +79,6 @@ public class FTPServer implements ApplicationListener return (filesysConfig != null && filesysConfig.isFTPServerEnabled()); } - /* - * (non-Javadoc) - * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent) - */ - public void onApplicationEvent(ApplicationEvent event) - { - if (event instanceof ContextRefreshedEvent) - { - try - { - startServer(); - } - catch (SocketException e) - { - throw new AlfrescoRuntimeException("Failed to start FTP server", e); - } - catch (IOException e) - { - throw new AlfrescoRuntimeException("Failed to start FTP server", e); - } - } - } - /** * Start the FTP server components * @@ -251,4 +228,28 @@ public class FTPServer implements ApplicationListener } System.exit(1); } + + @Override + protected void onBootstrap(ApplicationEvent event) + { + try + { + startServer(); + } + catch (SocketException e) + { + throw new AlfrescoRuntimeException("Failed to start FTP server", e); + } + catch (IOException e) + { + throw new AlfrescoRuntimeException("Failed to start FTP server", e); + } + } + + @Override + protected void onShutdown(ApplicationEvent event) + { + stopServer(); + } + } diff --git a/source/java/org/alfresco/filesys/ftp/FTPSrvSession.java b/source/java/org/alfresco/filesys/ftp/FTPSrvSession.java index 16a9daa11c..afd55c32a8 100644 --- a/source/java/org/alfresco/filesys/ftp/FTPSrvSession.java +++ b/source/java/org/alfresco/filesys/ftp/FTPSrvSession.java @@ -852,7 +852,7 @@ public class FTPSrvSession extends SrvSession implements Runnable // DEBUG if ( logger.isDebugEnabled()) - logger.debug("Logon failed", ex); + logger.debug("Logon failed for user " + cInfo.getUserName()); } // Check if the logon was successful diff --git a/source/java/org/alfresco/filesys/server/auth/AlfrescoAuthenticator.java b/source/java/org/alfresco/filesys/server/auth/AlfrescoAuthenticator.java index 817e7b361b..f26a8bfdd2 100644 --- a/source/java/org/alfresco/filesys/server/auth/AlfrescoAuthenticator.java +++ b/source/java/org/alfresco/filesys/server/auth/AlfrescoAuthenticator.java @@ -17,10 +17,17 @@ package org.alfresco.filesys.server.auth; import java.security.NoSuchAlgorithmException; +import net.sf.acegisecurity.Authentication; import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.auth.AuthContext; +import org.alfresco.filesys.server.auth.CifsAuthenticator; +import org.alfresco.filesys.server.auth.ClientInfo; +import org.alfresco.filesys.server.auth.NTLanManAuthContext; import org.alfresco.filesys.smb.server.SMBSrvSession; +import org.alfresco.filesys.util.HexDump; import org.alfresco.repo.security.authentication.NTLMMode; +import org.alfresco.repo.security.authentication.ntlm.NTLMPassthruToken; /** * Alfresco Authenticator Class @@ -88,10 +95,7 @@ public class AlfrescoAuthenticator extends CifsAuthenticator { // Use the existing authentication token - if ( client.isGuest()) - m_authComponent.setGuestUserAsCurrentUser(); - else - m_authComponent.setCurrentUser(mapUserNameToPerson(client.getUserName())); + m_authComponent.setCurrentUser(client.getUserName()); // Debug @@ -107,7 +111,7 @@ public class AlfrescoAuthenticator extends CifsAuthenticator int authSts = AUTH_DISALLOW; - if ( client.isGuest() || client.getUserName().equalsIgnoreCase(GUEST_USERNAME)) + if ( client.isGuest() || client.getUserName().equalsIgnoreCase(getGuestUserName())) { // Check if guest logons are allowed @@ -140,7 +144,13 @@ public class AlfrescoAuthenticator extends CifsAuthenticator authSts = doMD4UserAuthentication(client, sess, alg); } - + else + { + // Perform passthru authentication password check + + authSts = doPassthruUserAuthentication(client, sess, alg); + } + // Check if the logon status indicates a guest logon if ( authSts == AUTH_GUEST) @@ -172,6 +182,64 @@ public class AlfrescoAuthenticator extends CifsAuthenticator return authSts; } + /** + * Return an authentication context for the new session + * + * @return AuthContext + */ + public AuthContext getAuthContext( SMBSrvSession sess) + { + // Check if the client is already authenticated, and it is not a null logon + + AuthContext authCtx = null; + + if ( sess.hasAuthenticationContext() && sess.hasAuthenticationToken() && + sess.getClientInformation().getLogonType() != ClientInfo.LogonNull) + { + // Return the previous challenge, user is already authenticated + + authCtx = (NTLanManAuthContext) sess.getAuthenticationContext(); + + // DEBUG + + if ( logger.isDebugEnabled()) + logger.debug("Re-using existing challenge, already authenticated"); + } + else if ( m_authComponent.getNTLMMode() == NTLMMode.MD4_PROVIDER) + { + // Create a new authentication context for the session + + authCtx = new NTLanManAuthContext(); + sess.setAuthenticationContext( authCtx); + } + else + { + // Create an authentication token for the session + + NTLMPassthruToken authToken = new NTLMPassthruToken(); + + // Run the first stage of the passthru authentication to get the challenge + + m_authComponent.authenticate( authToken); + + // Save the authentication token for the second stage of the authentication + + sess.setAuthenticationToken(authToken); + + // Get the challenge from the token + + if ( authToken.getChallenge() != null) + { + authCtx = new NTLanManAuthContext( authToken.getChallenge().getBytes()); + sess.setAuthenticationContext( authCtx); + } + } + + // Return the authentication context + + return authCtx; + } + /** * Perform MD4 user authentication * @@ -217,6 +285,20 @@ public class AlfrescoAuthenticator extends CifsAuthenticator // Validate the password byte[] clientHash = client.getPassword(); + if ( clientHash == null || clientHash.length != 24) + { + // Use the secondary password hash from the client + + clientHash = client.getANSIPassword(); + + // DEBUG + + if ( logger.isDebugEnabled()) + { + logger.debug( "Using secondary password hash - " + HexDump.hexString(clientHash)); + logger.debug( " Local hash - " + HexDump.hexString( localHash)); + } + } if ( clientHash == null || clientHash.length != localHash.length) return CifsAuthenticator.AUTH_BADPASSWORD; @@ -229,7 +311,7 @@ public class AlfrescoAuthenticator extends CifsAuthenticator // Set the current user to be authenticated, save the authentication token - client.setAuthenticationToken( m_authComponent.setCurrentUser(mapUserNameToPerson(client.getUserName()))); + client.setAuthenticationToken( m_authComponent.setCurrentUser(client.getUserName())); // Get the users home folder node, if available @@ -259,4 +341,101 @@ public class AlfrescoAuthenticator extends CifsAuthenticator return allowGuest() ? CifsAuthenticator.AUTH_GUEST : CifsAuthenticator.AUTH_DISALLOW; } + + /** + * Perform passthru user authentication + * + * @param client Client information + * @param sess Server session + * @param alg Encryption algorithm + * @return int + */ + private final int doPassthruUserAuthentication(ClientInfo client, SrvSession sess, int alg) + { + // Get the authentication token for the session + + NTLMPassthruToken authToken = (NTLMPassthruToken) sess.getAuthenticationToken(); + + if ( authToken == null) + return CifsAuthenticator.AUTH_DISALLOW; + + // Get the appropriate hashed password for the algorithm + + int authSts = CifsAuthenticator.AUTH_DISALLOW; + byte[] hashedPassword = null; + + if ( alg == NTLM1) + hashedPassword = client.getPassword(); + else if ( alg == LANMAN) + hashedPassword = client.getANSIPassword(); + else + { + // Invalid/unsupported algorithm specified + + return CifsAuthenticator.AUTH_DISALLOW; + } + + // Set the username and hashed password in the authentication token + + authToken.setUserAndPassword( client.getUserName(), hashedPassword, alg); + + // Authenticate the user + + Authentication genAuthToken = null; + + try + { + // Run the second stage of the passthru authentication + + genAuthToken = m_authComponent.authenticate( authToken); + + // Check if the user has been logged on as a guest + + if (authToken.isGuestLogon()) + { + + // Check if the local server allows guest access + + if (allowGuest() == true) + { + + // Allow the user access as a guest + + authSts = CifsAuthenticator.AUTH_GUEST; + } + } + else + { + + // Allow the user full access to the server + + authSts = CifsAuthenticator.AUTH_ALLOW; + } + + // Set the current user to be authenticated, save the authentication token + + client.setAuthenticationToken( genAuthToken); + + // Get the users home folder node, if available + + getHomeFolderForUser( client); + + // DEBUG + + if ( logger.isDebugEnabled()) + logger.debug("Auth token " + genAuthToken); + } + catch ( Exception ex) + { + logger.error("Error during passthru authentication", ex); + } + + // Clear the authentication token + + sess.setAuthenticationToken(null); + + // Return the authentication status + + return authSts; + } } \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/auth/CifsAuthenticator.java b/source/java/org/alfresco/filesys/server/auth/CifsAuthenticator.java index 7815a7cf8f..3effae1145 100644 --- a/source/java/org/alfresco/filesys/server/auth/CifsAuthenticator.java +++ b/source/java/org/alfresco/filesys/server/auth/CifsAuthenticator.java @@ -470,7 +470,7 @@ public abstract class CifsAuthenticator // Store the client maximum buffer size, maximum multiplexed requests count and client // capability flags - sess.setClientMaximumBufferSize(maxBufSize); + sess.setClientMaximumBufferSize(maxBufSize != 0 ? maxBufSize : SMBSrvSession.DefaultBufferSize); sess.setClientMaximumMultiplex(maxMpx); sess.setClientCapabilities(capabs); diff --git a/source/java/org/alfresco/filesys/server/auth/ClientInfo.java b/source/java/org/alfresco/filesys/server/auth/ClientInfo.java index 56eec14aeb..421c840583 100644 --- a/source/java/org/alfresco/filesys/server/auth/ClientInfo.java +++ b/source/java/org/alfresco/filesys/server/auth/ClientInfo.java @@ -71,6 +71,10 @@ public class ClientInfo private Authentication m_authToken; + // Authentication ticket, used for web access without having to re-authenticate + + private String m_authTicket; + // Home folder node private NodeRef m_homeNode; @@ -286,6 +290,26 @@ public class ClientInfo { return m_authToken; } + + /** + * Check if the client has an authentication ticket + * + * @return boolean + */ + public final boolean hasAuthenticationTicket() + { + return m_authTicket != null ? true : false; + } + + /** + * Return the authentication ticket + * + * @return String + */ + public final String getAuthenticationTicket() + { + return m_authTicket; + } /** * Check if the client has a home folder node @@ -409,6 +433,16 @@ public class ClientInfo { m_authToken = token; } + + /** + * Set the authentication ticket + * + * @param ticket String + */ + public final void setAuthenticationTicket(String ticket) + { + m_authTicket = ticket; + } /** * Set the home folder node @@ -448,6 +482,12 @@ public class ClientInfo str.append(",token="); str.append(getAuthenticationToken()); } + + if ( hasAuthenticationTicket()) + { + str.append(",ticket="); + str.append(getAuthenticationTicket()); + } if (isGuest()) str.append(",Guest"); diff --git a/source/java/org/alfresco/filesys/server/auth/EnterpriseCifsAuthenticator.java b/source/java/org/alfresco/filesys/server/auth/EnterpriseCifsAuthenticator.java index c5647faff4..4f034ee1c7 100644 --- a/source/java/org/alfresco/filesys/server/auth/EnterpriseCifsAuthenticator.java +++ b/source/java/org/alfresco/filesys/server/auth/EnterpriseCifsAuthenticator.java @@ -584,7 +584,7 @@ public class EnterpriseCifsAuthenticator extends CifsAuthenticator implements Ca // Store the client maximum buffer size, maximum multiplexed requests count and client capability flags - sess.setClientMaximumBufferSize(maxBufSize); + sess.setClientMaximumBufferSize(maxBufSize != 0 ? maxBufSize : SMBSrvSession.DefaultBufferSize); sess.setClientMaximumMultiplex(maxMpx); sess.setClientCapabilities(capabs); diff --git a/source/java/org/alfresco/filesys/server/auth/ntlm/AlfrescoAuthenticator.java b/source/java/org/alfresco/filesys/server/auth/ntlm/AlfrescoAuthenticator.java deleted file mode 100644 index 5b77d4171d..0000000000 --- a/source/java/org/alfresco/filesys/server/auth/ntlm/AlfrescoAuthenticator.java +++ /dev/null @@ -1,427 +0,0 @@ -/* - * Copyright (C) 2005-2006 Alfresco, Inc. - * - * Licensed under the Mozilla Public License version 1.1 - * with a permitted attribution clause. You may obtain a - * copy of the License at - * - * http://www.alfresco.org/legal/license.txt - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the specific - * language governing permissions and limitations under the - * License. - */ -package org.alfresco.filesys.server.auth.ntlm; - -import java.security.NoSuchAlgorithmException; -import net.sf.acegisecurity.Authentication; - -import org.alfresco.filesys.server.SrvSession; -import org.alfresco.filesys.server.auth.AuthContext; -import org.alfresco.filesys.server.auth.CifsAuthenticator; -import org.alfresco.filesys.server.auth.ClientInfo; -import org.alfresco.filesys.server.auth.NTLanManAuthContext; -import org.alfresco.filesys.smb.server.SMBSrvSession; -import org.alfresco.filesys.util.DataPacker; -import org.alfresco.repo.security.authentication.NTLMMode; -import org.alfresco.repo.security.authentication.ntlm.NTLMPassthruToken; - -/** - * Alfresco Authenticator Class - * - *

    The Alfresco authenticator implementation enables user level security mode using the Alfresco authentication - * component. - * - *

    Note: Switching off encrypted password support will cause later NT4 service pack releases and - * Win2000 to refuse to connect to the server without a registry update on the client. - * - * @author GKSpencer - */ -public class AlfrescoAuthenticator extends CifsAuthenticator -{ - /** - * Default Constructor - * - *

    Default to user mode security with encrypted password support. - */ - public AlfrescoAuthenticator() - { - } - - /** - * Validate that the authentication component supports the required mode - * - * @return boolean - */ - protected boolean validateAuthenticationMode() - { - // Make sure the authentication component supports MD4 hashed passwords or passthru mode - - if ( m_authComponent.getNTLMMode() != NTLMMode.MD4_PROVIDER && - m_authComponent.getNTLMMode() != NTLMMode.PASS_THROUGH) - return false; - return true; - } - - /** - * Authenticate a user - * - * @param client Client information - * @param sess Server session - * @param alg Encryption algorithm - */ - public int authenticateUser(ClientInfo client, SrvSession sess, int alg) - { - // Check if this is an SMB/CIFS null session logon. - // - // The null session will only be allowed to connect to the IPC$ named pipe share. - - if (client.isNullSession() && sess instanceof SMBSrvSession) - { - // Debug - - if ( logger.isDebugEnabled()) - logger.debug("Null CIFS logon allowed"); - - return CifsAuthenticator.AUTH_ALLOW; - } - - // Check if the client is already authenticated, and it is not a null logon - - if ( client.getAuthenticationToken() != null && client.getLogonType() != ClientInfo.LogonNull) - { - // Use the existing authentication token - - m_authComponent.setCurrentUser(client.getUserName()); - - // Debug - - if ( logger.isDebugEnabled()) - logger.debug("Re-using existing authentication token"); - - // Return the authentication status - - return client.getLogonType() != ClientInfo.LogonGuest ? AUTH_ALLOW : AUTH_GUEST; - } - - // Check if this is a guest logon - - int authSts = AUTH_DISALLOW; - - if ( client.isGuest() || client.getUserName().equalsIgnoreCase(getGuestUserName())) - { - // Check if guest logons are allowed - - if ( allowGuest() == false) - return AUTH_DISALLOW; - - // Get a guest authentication token - - doGuestLogon( client, sess); - - // Indicate logged on as guest - - authSts = AUTH_GUEST; - - // DEBUG - - if ( logger.isDebugEnabled()) - logger.debug("Authenticated user " + client.getUserName() + " sts=" + getStatusAsString(authSts)); - - // Return the guest status - - return authSts; - } - - // Check if MD4 or passthru mode is configured - - else if ( m_authComponent.getNTLMMode() == NTLMMode.MD4_PROVIDER) - { - // Perform local MD4 password check - - authSts = doMD4UserAuthentication(client, sess, alg); - } - else - { - // Perform passthru authentication password check - - authSts = doPassthruUserAuthentication(client, sess, alg); - } - - // Check if the logon status indicates a guest logon - - if ( authSts == AUTH_GUEST) - { - // Only allow the guest logon if user mapping is enabled - - if ( mapUnknownUserToGuest()) - { - // Logon as guest, setup the security context - - doGuestLogon( client, sess); - } - else - { - // Do not allow the guest logon - - authSts = AUTH_DISALLOW; - } - } - - // DEBUG - - if ( logger.isDebugEnabled()) - logger.debug("Authenticated user " + client.getUserName() + " sts=" + getStatusAsString(authSts) + - " via " + (m_authComponent.getNTLMMode() == NTLMMode.MD4_PROVIDER ? "MD4" : "Passthru")); - - // Return the authentication status - - return authSts; - } - - /** - * Return an authentication context for the new session - * - * @return AuthContext - */ - public AuthContext getAuthContext( SMBSrvSession sess) - { - // Check if the client is already authenticated, and it is not a null logon - - AuthContext authCtx = null; - - if ( sess.hasAuthenticationContext() && sess.hasAuthenticationToken() && - sess.getClientInformation().getLogonType() != ClientInfo.LogonNull) - { - // Return the previous challenge, user is already authenticated - - authCtx = (NTLanManAuthContext) sess.getAuthenticationContext(); - - // DEBUG - - if ( logger.isDebugEnabled()) - logger.debug("Re-using existing challenge, already authenticated"); - } - else if ( m_authComponent.getNTLMMode() == NTLMMode.MD4_PROVIDER) - { - // Create a new authentication context for the session - - authCtx = new NTLanManAuthContext(); - sess.setAuthenticationContext( authCtx); - } - else - { - // Create an authentication token for the session - - NTLMPassthruToken authToken = new NTLMPassthruToken(); - - // Run the first stage of the passthru authentication to get the challenge - - m_authComponent.authenticate( authToken); - - // Save the authentication token for the second stage of the authentication - - sess.setAuthenticationToken(authToken); - - // Get the challenge from the token - - if ( authToken.getChallenge() != null) - { - authCtx = new NTLanManAuthContext( authToken.getChallenge().getBytes()); - sess.setAuthenticationContext( authCtx); - } - } - - // Return the authentication context - - return authCtx; - } - - /** - * Perform MD4 user authentication - * - * @param client Client information - * @param sess Server session - * @param alg Encryption algorithm - * @return int - */ - private final int doMD4UserAuthentication(ClientInfo client, SrvSession sess, int alg) - { - // Get the stored MD4 hashed password for the user, or null if the user does not exist - - String md4hash = m_authComponent.getMD4HashedPassword(client.getUserName()); - - if ( md4hash != null) - { - // Check if the client has supplied an NTLM hashed password, if not then do not allow access - - if ( client.getPassword() == null) - return CifsAuthenticator.AUTH_BADPASSWORD; - - try - { - // Generate the local encrypted password using the challenge that was sent to the client - - byte[] p21 = new byte[21]; - byte[] md4byts = m_md4Encoder.decodeHash(md4hash); - System.arraycopy(md4byts, 0, p21, 0, 16); - - // Get the challenge that was sent to the client - - NTLanManAuthContext authCtx = null; - - if ( sess.hasAuthenticationContext() && sess.getAuthenticationContext() instanceof NTLanManAuthContext) - authCtx = (NTLanManAuthContext) sess.getAuthenticationContext(); - else - return CifsAuthenticator.AUTH_DISALLOW; - - // Generate the local hash of the password using the same challenge - - byte[] localHash = getEncryptor().doNTLM1Encryption(p21, authCtx.getChallenge()); - - // Validate the password - - byte[] clientHash = client.getPassword(); - - if ( clientHash == null || clientHash.length != localHash.length) - return CifsAuthenticator.AUTH_BADPASSWORD; - - for ( int i = 0; i < clientHash.length; i++) - { - if ( clientHash[i] != localHash[i]) - return CifsAuthenticator.AUTH_BADPASSWORD; - } - - // Set the current user to be authenticated, save the authentication token - - client.setAuthenticationToken( m_authComponent.setCurrentUser(client.getUserName())); - - // Get the users home folder node, if available - - getHomeFolderForUser( client); - - // Passwords match, grant access - - return CifsAuthenticator.AUTH_ALLOW; - } - catch (NoSuchAlgorithmException ex) - { - } - - // Error during password check, do not allow access - - return CifsAuthenticator.AUTH_DISALLOW; - } - - // Check if this is an SMB/CIFS null session logon. - // - // The null session will only be allowed to connect to the IPC$ named pipe share. - - if (client.isNullSession() && sess instanceof SMBSrvSession) - return CifsAuthenticator.AUTH_ALLOW; - - // User does not exist, check if guest access is allowed - - return allowGuest() ? CifsAuthenticator.AUTH_GUEST : CifsAuthenticator.AUTH_DISALLOW; - } - - /** - * Perform passthru user authentication - * - * @param client Client information - * @param sess Server session - * @param alg Encryption algorithm - * @return int - */ - private final int doPassthruUserAuthentication(ClientInfo client, SrvSession sess, int alg) - { - // Get the authentication token for the session - - NTLMPassthruToken authToken = (NTLMPassthruToken) sess.getAuthenticationToken(); - - if ( authToken == null) - return CifsAuthenticator.AUTH_DISALLOW; - - // Get the appropriate hashed password for the algorithm - - int authSts = CifsAuthenticator.AUTH_DISALLOW; - byte[] hashedPassword = null; - - if ( alg == NTLM1) - hashedPassword = client.getPassword(); - else if ( alg == LANMAN) - hashedPassword = client.getANSIPassword(); - else - { - // Invalid/unsupported algorithm specified - - return CifsAuthenticator.AUTH_DISALLOW; - } - - // Set the username and hashed password in the authentication token - - authToken.setUserAndPassword( client.getUserName(), hashedPassword, alg); - - // Authenticate the user - - Authentication genAuthToken = null; - - try - { - // Run the second stage of the passthru authentication - - genAuthToken = m_authComponent.authenticate( authToken); - - // Check if the user has been logged on as a guest - - if (authToken.isGuestLogon()) - { - - // Check if the local server allows guest access - - if (allowGuest() == true) - { - - // Allow the user access as a guest - - authSts = CifsAuthenticator.AUTH_GUEST; - } - } - else - { - - // Allow the user full access to the server - - authSts = CifsAuthenticator.AUTH_ALLOW; - } - - // Set the current user to be authenticated, save the authentication token - - client.setAuthenticationToken( genAuthToken); - - // Get the users home folder node, if available - - getHomeFolderForUser( client); - - // DEBUG - - if ( logger.isDebugEnabled()) - logger.debug("Auth token " + genAuthToken); - } - catch ( Exception ex) - { - logger.error("Error during passthru authentication", ex); - } - - // Clear the authentication token - - sess.setAuthenticationToken(null); - - // Return the authentication status - - return authSts; - } -} \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/auth/ntlm/NTLMLogonDetails.java b/source/java/org/alfresco/filesys/server/auth/ntlm/NTLMLogonDetails.java index c5c8f7f314..4615408bd5 100644 --- a/source/java/org/alfresco/filesys/server/auth/ntlm/NTLMLogonDetails.java +++ b/source/java/org/alfresco/filesys/server/auth/ntlm/NTLMLogonDetails.java @@ -39,6 +39,10 @@ public class NTLMLogonDetails private String m_authSrvAddr; + // Date/time authentication was started + + private long m_createTime; + // Date/time the user was authenticated private long m_authTime; @@ -61,6 +65,7 @@ public class NTLMLogonDetails */ public NTLMLogonDetails() { + m_createTime = System.currentTimeMillis(); } /** @@ -74,6 +79,8 @@ public class NTLMLogonDetails */ public NTLMLogonDetails(String user, String wks, String domain, boolean guest, String authSrv) { + m_createTime = System.currentTimeMillis(); + setDetails(user, wks, domain, guest, authSrv); } @@ -117,6 +124,16 @@ public class NTLMLogonDetails return m_authSrvAddr; } + /** + * Return the date/time the authentication was started + * + * @return long + */ + public final long createdAt() + { + return m_createTime; + } + /** * Return the date/time the user was authenticated * diff --git a/source/java/org/alfresco/filesys/server/config/ServerConfiguration.java b/source/java/org/alfresco/filesys/server/config/ServerConfiguration.java index 46c7e16839..a48040b950 100644 --- a/source/java/org/alfresco/filesys/server/config/ServerConfiguration.java +++ b/source/java/org/alfresco/filesys/server/config/ServerConfiguration.java @@ -81,11 +81,10 @@ import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.security.AuthenticationService; import org.alfresco.service.cmr.security.PersonService; import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.AbstractLifecycleBean; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationEvent; -import org.springframework.context.ApplicationListener; -import org.springframework.context.event.ContextRefreshedEvent; /** *

    @@ -93,7 +92,7 @@ import org.springframework.context.event.ContextRefreshedEvent; * * @author Gary K. Spencer */ -public class ServerConfiguration implements ApplicationListener +public class ServerConfiguration extends AbstractLifecycleBean { // Debug logging @@ -425,18 +424,6 @@ public class ServerConfiguration implements ApplicationListener return initialised; } - /* - * (non-Javadoc) - * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent) - */ - public void onApplicationEvent(ApplicationEvent event) - { - if (event instanceof ContextRefreshedEvent) - { - init(); - } - } - /** * Initialize the configuration using the configuration service */ @@ -1791,9 +1778,7 @@ public class ServerConfiguration implements ApplicationListener // Load the Alfresco authenticator dynamically - auth = loadAuthenticatorClass("org.alfresco.filesys.server.auth.ntlm.AlfrescoAuthenticator"); - if ( auth == null) - auth = loadAuthenticatorClass("org.alfresco.filesys.server.auth.AlfrescoAuthenticator"); + auth = loadAuthenticatorClass("org.alfresco.filesys.server.auth.AlfrescoAuthenticator"); if ( auth == null) throw new AlfrescoRuntimeException("Failed to load Alfresco authenticator"); @@ -3359,4 +3344,17 @@ public class ServerConfiguration implements ApplicationListener return srvAuth; } + + @Override + protected void onBootstrap(ApplicationEvent event) + { + init(); + } + + @Override + protected void onShutdown(ApplicationEvent event) + { + // NO-OP + } + } \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/smb/server/repo/ContentDiskDriver.java b/source/java/org/alfresco/filesys/smb/server/repo/ContentDiskDriver.java index 049e5d4e7d..7ce6b8705f 100644 --- a/source/java/org/alfresco/filesys/smb/server/repo/ContentDiskDriver.java +++ b/source/java/org/alfresco/filesys/smb/server/repo/ContentDiskDriver.java @@ -18,6 +18,7 @@ package org.alfresco.filesys.smb.server.repo; import java.io.FileNotFoundException; import java.io.IOException; +import java.net.InetAddress; import java.util.List; import javax.transaction.UserTransaction; @@ -58,7 +59,6 @@ import org.alfresco.filesys.util.WildCard; import org.alfresco.model.ContentModel; import org.alfresco.repo.security.authentication.AuthenticationComponent; import org.alfresco.service.ServiceRegistry; -import org.alfresco.service.cmr.coci.CheckOutCheckInService; import org.alfresco.service.cmr.lock.NodeLockedException; import org.alfresco.service.cmr.repository.ContentService; import org.alfresco.service.cmr.repository.NodeRef; @@ -66,6 +66,7 @@ import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.service.cmr.search.SearchService; import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.AuthenticationService; import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.transaction.TransactionService; @@ -91,6 +92,10 @@ public class ContentDiskDriver implements DiskInterface, IOCtlInterface private static final String KEY_ROOT_PATH = "rootPath"; private static final String KEY_RELATIVE_PATH = "relativePath"; + // Token name to substitute current servers DNS name or TCP/IP address into the webapp URL + + private static final String TokenLocalName = "${localname}"; + // Services and helpers private CifsHelper cifsHelper; @@ -102,6 +107,7 @@ public class ContentDiskDriver implements DiskInterface, IOCtlInterface private PermissionService permissionService; private AuthenticationComponent authComponent; + private AuthenticationService authService; // Service registry for desktop actions @@ -127,6 +133,16 @@ public class ContentDiskDriver implements DiskInterface, IOCtlInterface return this.cifsHelper; } + /** + * Return the authentication service + * + * @return AuthenticationService + */ + public final AuthenticationService getAuthenticationService() + { + return authService; + } + /** * Return the transaction service * @@ -185,7 +201,7 @@ public class ContentDiskDriver implements DiskInterface, IOCtlInterface { return this.serviceRegistry; } - + /** * @param contentService the content service */ @@ -255,6 +271,16 @@ public class ContentDiskDriver implements DiskInterface, IOCtlInterface { this.authComponent = authComponent; } + + /** + * Set the authentication service + * + * @param authService AuthenticationService + */ + public void setAuthenticationService(AuthenticationService authService) + { + this.authService = authService; + } /** * Parse and validate the parameter string and create a device context object for this instance @@ -418,7 +444,39 @@ public class ContentDiskDriver implements DiskInterface, IOCtlInterface if ( pseudoName.getValue().endsWith(".url") == false) throw new DeviceContextException("URL link file must end with .url, " + pseudoName.getValue()); - // Set the URL link file name and web path + // Check if the URL path name contains the local name token + + int pos = path.indexOf(TokenLocalName); + if (pos != -1) + { + + // Get the local server name + + String srvName = "localhost"; + + try + { + srvName = InetAddress.getLocalHost().getHostName(); + } + catch ( Exception ex) + { + } + + // Rebuild the host name substituting the token with the local server name + + StringBuilder hostStr = new StringBuilder(); + + hostStr.append( path.substring(0, pos)); + hostStr.append(srvName); + + pos += TokenLocalName.length(); + if (pos < path.length()) + hostStr.append( path.substring(pos)); + + path = hostStr.toString(); + } + + // Set the URL link file name and web path context.setURLFileName( pseudoName.getValue()); context.setURLPrefix( path); diff --git a/source/java/org/alfresco/filesys/smb/server/repo/ContentIOControlHandler.java b/source/java/org/alfresco/filesys/smb/server/repo/ContentIOControlHandler.java index d9cb60e028..f83cad068a 100644 --- a/source/java/org/alfresco/filesys/smb/server/repo/ContentIOControlHandler.java +++ b/source/java/org/alfresco/filesys/smb/server/repo/ContentIOControlHandler.java @@ -19,6 +19,7 @@ package org.alfresco.filesys.smb.server.repo; import java.io.FileNotFoundException; import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.auth.ClientInfo; import org.alfresco.filesys.server.filesys.IOControlNotImplementedException; import org.alfresco.filesys.server.filesys.NetworkFile; import org.alfresco.filesys.server.filesys.TreeConnection; @@ -30,10 +31,12 @@ import org.alfresco.filesys.smb.server.repo.ContentDiskDriver; import org.alfresco.filesys.smb.server.repo.IOControlHandler; import org.alfresco.filesys.util.DataBuffer; import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.authentication.AuthenticationException; import org.alfresco.service.cmr.lock.LockType; import org.alfresco.service.cmr.repository.ContentData; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AuthenticationService; import org.alfresco.service.transaction.TransactionService; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -85,6 +88,16 @@ public class ContentIOControlHandler implements IOControlHandler return contentDriver.getCifsHelper(); } + /** + * Return the authentication service + * + * @return AuthenticationService + */ + public final AuthenticationService getAuthenticationService() + { + return contentDriver.getAuthenticationService(); + } + /** * Return the transaction service * @@ -511,6 +524,11 @@ public class ContentIOControlHandler implements IOControlHandler // Start a transaction sess.beginTransaction( getTransactionService(), true); + + // Get an authentication ticket for the client, or validate the existing ticket. The ticket can be used when + // generating URLs for the client-side application so that the user does not have to re-authenticate + + getTicketForClient( sess); // Get the list of targets for the action @@ -624,4 +642,65 @@ public class ContentIOControlHandler implements IOControlHandler return respBuf; } + + /** + * Get, or validate, an authentication ticket for the client + * + * @param sess SrvSession + */ + private final void getTicketForClient(SrvSession sess) + { + // Get the client information and check if there is a ticket allocated + + ClientInfo cInfo = sess.getClientInformation(); + if ( cInfo == null) + return; + + boolean needTicket = true; + + if ( cInfo.hasAuthenticationTicket()) + { + // Validate the existing ticket, it may have expired + + try + { + // Validate the existing ticket + + getAuthenticationService().validate( cInfo.getAuthenticationTicket()); + needTicket = false; + } + catch ( AuthenticationException ex) + { + // Invalidate the current ticket + + try + { + getAuthenticationService().invalidateTicket( cInfo.getAuthenticationTicket()); + cInfo.setAuthenticationTicket( null); + } + catch (Exception ex2) + { + // DEBUG + + if ( logger.isDebugEnabled()) + logger.debug("Error during invalidate ticket", ex2); + } + + // DEBUG + + if ( logger.isDebugEnabled()) + logger.debug("Auth ticket expired or invalid"); + } + } + + // Check if a ticket needs to be allocated + + if ( needTicket == true) + { + // Allocate a new ticket and store in the client information for this session + + String ticket = getAuthenticationService().getCurrentTicket(); + cInfo.setAuthenticationTicket( ticket); + } + } } diff --git a/source/java/org/alfresco/filesys/smb/server/repo/DesktopAction.java b/source/java/org/alfresco/filesys/smb/server/repo/DesktopAction.java index 1bcc759564..e5ca0a28e7 100644 --- a/source/java/org/alfresco/filesys/smb/server/repo/DesktopAction.java +++ b/source/java/org/alfresco/filesys/smb/server/repo/DesktopAction.java @@ -19,10 +19,12 @@ package org.alfresco.filesys.smb.server.repo; import java.io.File; import java.io.UnsupportedEncodingException; +import java.net.InetAddress; import java.net.URL; import java.net.URLDecoder; import org.alfresco.config.ConfigElement; +import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.filesys.server.filesys.DiskSharedDevice; import org.alfresco.filesys.smb.server.repo.pseudo.LocalPseudoFile; import org.alfresco.filesys.smb.server.repo.pseudo.PseudoFile; @@ -88,7 +90,11 @@ public abstract class DesktopAction { public static final int StsLaunchURL = 7; public static final int StsCommandLine = 8; - // Action name + // Token name to substitute current servers DNS name or TCP/IP address into the webapp URL + + private static final String TokenLocalName = "${localname}"; + + // Action name private String m_name; @@ -109,6 +115,10 @@ public abstract class DesktopAction { private ContentDiskDriver m_contentDriver; private ContentContext m_contentContext; + // Webapp URL + + private String m_webappURL; + // Debug enable flag private boolean m_debug; @@ -254,6 +264,26 @@ public abstract class DesktopAction { return m_debug; } + /** + * Check if the webapp URL is set + * + * @return boolean + */ + public final boolean hasWebappURL() + { + return m_webappURL != null ? true : false; + } + + /** + * Return the webapp URL + * + * @return String + */ + public final String getWebappURL() + { + return m_webappURL; + } + /** * Initialize the desktop action * @@ -348,7 +378,50 @@ public abstract class DesktopAction { if ( findConfigElement("noConfirm", global, config) != null && hasPreProcessAction(PreConfirmAction)) setPreProcessActions(getPreProcessActions() - PreConfirmAction); + + // Check if the webapp URL has been specified + + ConfigElement webURL = findConfigElement("webpath", global, config); + if ( webURL != null) + { + // Check if the path name contains the local name token + + String webPath = webURL.getValue(); + if ( webPath.endsWith("/") == false) + webPath = webPath + "/"; + int pos = webPath.indexOf(TokenLocalName); + if (pos != -1) + { + + // Get the local server name + + String srvName = "localhost"; + + try + { + srvName = InetAddress.getLocalHost().getHostName(); + } + catch ( Exception ex) + { + } + + // Rebuild the host name substituting the token with the local server name + + StringBuilder hostStr = new StringBuilder(); + + hostStr.append(webPath.substring(0, pos)); + hostStr.append(srvName); + + pos += TokenLocalName.length(); + if (pos < webPath.length()) + hostStr.append(webPath.substring(pos)); + + webPath = hostStr.toString(); + setWebappURL( webPath); + } + } + // Check if debug output is enabled for the action ConfigElement debug = findConfigElement("debug", global, config); @@ -511,6 +584,16 @@ public abstract class DesktopAction { m_debug = ena; } + /** + * Set the webapp URL + * + * @param urlStr String + */ + public final void setWebappURL(String urlStr) + { + m_webappURL = urlStr; + } + /** * Equality check * diff --git a/source/java/org/alfresco/filesys/smb/server/repo/DesktopParams.java b/source/java/org/alfresco/filesys/smb/server/repo/DesktopParams.java index 1a2e294135..6a45926dc2 100644 --- a/source/java/org/alfresco/filesys/smb/server/repo/DesktopParams.java +++ b/source/java/org/alfresco/filesys/smb/server/repo/DesktopParams.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.List; import org.alfresco.filesys.server.SrvSession; +import org.alfresco.filesys.server.auth.ClientInfo; import org.alfresco.filesys.server.filesys.NetworkFile; import org.alfresco.service.cmr.repository.NodeRef; @@ -88,6 +89,19 @@ public class DesktopParams { { return m_session; } + + /** + * Return the authentication ticket for the user/session + * + * @return String + */ + public final String getTicket() + { + ClientInfo cInfo = m_session.getClientInformation(); + if ( cInfo != null) + return cInfo.getAuthenticationTicket(); + return null; + } /** * Return the working directory node diff --git a/source/java/org/alfresco/filesys/smb/server/repo/desk/JavaScriptDesktopAction.java b/source/java/org/alfresco/filesys/smb/server/repo/desk/JavaScriptDesktopAction.java index 303ff36272..1a361e62f2 100644 --- a/source/java/org/alfresco/filesys/smb/server/repo/desk/JavaScriptDesktopAction.java +++ b/source/java/org/alfresco/filesys/smb/server/repo/desk/JavaScriptDesktopAction.java @@ -284,6 +284,11 @@ public class JavaScriptDesktopAction extends DesktopAction { model.put("deskParams", params); model.put("out", System.out); + // Add the webapp URL, if valid + + if ( hasWebappURL()) + model.put("webURL", getWebappURL()); + // Start a transaction params.getSession().beginTransaction(getTransactionService(), false); diff --git a/source/java/org/alfresco/filesys/smb/server/repo/desk/URLDesktopAction.java b/source/java/org/alfresco/filesys/smb/server/repo/desk/URLDesktopAction.java index ca63252963..2a91523ae7 100644 --- a/source/java/org/alfresco/filesys/smb/server/repo/desk/URLDesktopAction.java +++ b/source/java/org/alfresco/filesys/smb/server/repo/desk/URLDesktopAction.java @@ -16,6 +16,9 @@ */ package org.alfresco.filesys.smb.server.repo.desk; +import java.net.InetAddress; +import java.net.UnknownHostException; + import org.alfresco.filesys.smb.server.repo.DesktopAction; import org.alfresco.filesys.smb.server.repo.DesktopParams; import org.alfresco.filesys.smb.server.repo.DesktopResponse; @@ -45,8 +48,29 @@ public class URLDesktopAction extends DesktopAction { @Override public DesktopResponse runAction(DesktopParams params) { - // Return a URL in the status message + // Get the local IP address - return new DesktopResponse(StsLaunchURL, "http://www.alfresco.com"); + String ipAddr = null; + + try + { + ipAddr = InetAddress.getLocalHost().getHostAddress(); + } + catch (UnknownHostException ex) + { + } + + // Return a URL in the status message to browse to the folder node + + StringBuilder urlStr = new StringBuilder(); + + urlStr.append( "http://"); + urlStr.append(ipAddr); + urlStr.append(":8080/alfresco/navigate/browse/workspace/SpacesStore/"); + urlStr.append( params.getFolderNode().getId()); + urlStr.append("?ticket="); + urlStr.append(params.getTicket()); + + return new DesktopResponse(StsLaunchURL, urlStr.toString()); } } diff --git a/source/java/org/alfresco/filesys/smb/server/repo/pseudo/MemoryNetworkFile.java b/source/java/org/alfresco/filesys/smb/server/repo/pseudo/MemoryNetworkFile.java index 76d7cafe63..8eb403b31f 100644 --- a/source/java/org/alfresco/filesys/smb/server/repo/pseudo/MemoryNetworkFile.java +++ b/source/java/org/alfresco/filesys/smb/server/repo/pseudo/MemoryNetworkFile.java @@ -19,7 +19,6 @@ package org.alfresco.filesys.smb.server.repo.pseudo; import java.io.IOException; -import org.alfresco.filesys.server.filesys.AccessDeniedException; import org.alfresco.filesys.server.filesys.FileInfo; import org.alfresco.filesys.server.filesys.NetworkFile; import org.alfresco.filesys.smb.SeekType; @@ -222,9 +221,7 @@ public class MemoryNetworkFile extends NetworkFile */ public void truncateFile(long siz) throws IOException { - // Do not allow the file to be written to - - throw new AccessDeniedException("Cannot truncate pseudo file"); + // Allow the truncate, do not alter the pseduo file data } /** @@ -236,9 +233,7 @@ public class MemoryNetworkFile extends NetworkFile */ public void writeFile(byte[] buf, int len, int pos) throws java.io.IOException { - // Do not allow the file to be written to - - throw new AccessDeniedException("Cannot write to pseudo file"); + // Allow the write, just do not do anything } /** @@ -252,8 +247,6 @@ public class MemoryNetworkFile extends NetworkFile */ public void writeFile(byte[] buf, int len, int pos, long offset) throws java.io.IOException { - // Do not allow the file to be written to - - throw new AccessDeniedException("Cannot write to pseudo file"); + // Allow the write, just do not do anything } } diff --git a/source/java/org/alfresco/filesys/smb/server/repo/pseudo/PseudoNetworkFile.java b/source/java/org/alfresco/filesys/smb/server/repo/pseudo/PseudoNetworkFile.java index 0ae631897c..4bcd2cefe0 100644 --- a/source/java/org/alfresco/filesys/smb/server/repo/pseudo/PseudoNetworkFile.java +++ b/source/java/org/alfresco/filesys/smb/server/repo/pseudo/PseudoNetworkFile.java @@ -21,7 +21,6 @@ import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; -import org.alfresco.filesys.server.filesys.AccessDeniedException; import org.alfresco.filesys.server.filesys.NetworkFile; import org.alfresco.filesys.smb.SeekType; @@ -272,9 +271,7 @@ public class PseudoNetworkFile extends NetworkFile */ public void truncateFile(long siz) throws IOException { - // Do not allow the file to be written to - - throw new AccessDeniedException("Cannot truncate pseudo file"); + // Allow the truncate, just do not do anything } /** @@ -286,9 +283,7 @@ public class PseudoNetworkFile extends NetworkFile */ public void writeFile(byte[] buf, int len, int pos) throws java.io.IOException { - // Do not allow the file to be written to - - throw new AccessDeniedException("Cannot write to pseudo file"); + // Allow the write, just do not do anything } /** @@ -302,8 +297,6 @@ public class PseudoNetworkFile extends NetworkFile */ public void writeFile(byte[] buf, int len, int pos, long offset) throws java.io.IOException { - // Do not allow the file to be written to - - throw new AccessDeniedException("Cannot write to pseudo file"); + // Allow the write, just do not do anything } } diff --git a/source/java/org/alfresco/jcr/exporter/JCRSystemXMLExporter.java b/source/java/org/alfresco/jcr/exporter/JCRSystemXMLExporter.java index a9d3b3e9bf..8f973ed183 100644 --- a/source/java/org/alfresco/jcr/exporter/JCRSystemXMLExporter.java +++ b/source/java/org/alfresco/jcr/exporter/JCRSystemXMLExporter.java @@ -178,7 +178,7 @@ public class JCRSystemXMLExporter implements Exporter Value[] mixinValues = mixinTypes.getValues(); for (int i = 0; i < mixinValues.length; i++) { - value(nodeRef, JCRMixinTypesProperty.PROPERTY_NAME, mixinValues[i], i); + value(nodeRef, JCRMixinTypesProperty.PROPERTY_NAME, mixinValues[i].getString(), i); } endProperty(nodeRef, JCRMixinTypesProperty.PROPERTY_NAME); diff --git a/source/java/org/alfresco/repo/action/executer/CopyActionExecuter.java b/source/java/org/alfresco/repo/action/executer/CopyActionExecuter.java index 1c1d92ebc4..229a844a9e 100644 --- a/source/java/org/alfresco/repo/action/executer/CopyActionExecuter.java +++ b/source/java/org/alfresco/repo/action/executer/CopyActionExecuter.java @@ -162,7 +162,7 @@ public class CopyActionExecuter extends ActionExecuterAbstractBase else { // Create a new copy of the node - this.copyService.copy( + this.copyService.copyAndRename( actionedUponNodeRef, destinationParent, destinationAssocTypeQName, diff --git a/source/java/org/alfresco/repo/admin/patch/AbstractPatch.java b/source/java/org/alfresco/repo/admin/patch/AbstractPatch.java index 4375b6e101..b921a3e0f7 100644 --- a/source/java/org/alfresco/repo/admin/patch/AbstractPatch.java +++ b/source/java/org/alfresco/repo/admin/patch/AbstractPatch.java @@ -22,6 +22,7 @@ import java.util.Collections; import java.util.List; import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.node.integrity.IntegrityChecker; import org.alfresco.repo.security.authentication.AuthenticationComponent; import org.alfresco.repo.transaction.TransactionUtil; import org.alfresco.repo.transaction.TransactionUtil.TransactionWork; @@ -329,6 +330,9 @@ public abstract class AbstractPatch implements Patch { public String doWork() throws Exception { + // downgrade integrity checking + IntegrityChecker.setWarnInTransaction(); + String report = applyInternal(); // done return report; @@ -389,7 +393,8 @@ public abstract class AbstractPatch implements Patch /** * This method does the work. All transactions and thread-safety will be taken care of by this class. - * Any exception will result in the transaction being rolled back. + * Any exception will result in the transaction being rolled back. Integrity checks are downgraded + * for the duration of the transaction. * * @return Returns the report (only success messages). * @see #apply() diff --git a/source/java/org/alfresco/repo/admin/patch/PatchExecuter.java b/source/java/org/alfresco/repo/admin/patch/PatchExecuter.java index d0f2655d69..dbb5e0b592 100644 --- a/source/java/org/alfresco/repo/admin/patch/PatchExecuter.java +++ b/source/java/org/alfresco/repo/admin/patch/PatchExecuter.java @@ -21,11 +21,10 @@ import java.util.List; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.i18n.I18NUtil; +import org.alfresco.util.AbstractLifecycleBean; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationEvent; -import org.springframework.context.ApplicationListener; -import org.springframework.context.event.ContextRefreshedEvent; /** * This component is responsible for ensuring that patches are applied @@ -33,7 +32,7 @@ import org.springframework.context.event.ContextRefreshedEvent; * * @author Derek Hulley */ -public class PatchExecuter implements ApplicationListener +public class PatchExecuter extends AbstractLifecycleBean { private static final String MSG_CHECKING = "patch.executer.checking"; private static final String MSG_NO_PATCHES_REQUIRED = "patch.executer.no_patches_required"; @@ -101,16 +100,16 @@ public class PatchExecuter implements ApplicationListener } } - /* - * (non-Javadoc) - * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent) - */ - public void onApplicationEvent(ApplicationEvent event) + @Override + protected void onBootstrap(ApplicationEvent event) { - if (event instanceof ContextRefreshedEvent) - { - applyOutstandingPatches(); - } + applyOutstandingPatches(); + } + + @Override + protected void onShutdown(ApplicationEvent event) + { + // NOOP } } diff --git a/source/java/org/alfresco/repo/admin/patch/impl/AbstractPermissionChangePatch.java b/source/java/org/alfresco/repo/admin/patch/impl/AbstractPermissionChangePatch.java new file mode 100644 index 0000000000..7f0cc516de --- /dev/null +++ b/source/java/org/alfresco/repo/admin/patch/impl/AbstractPermissionChangePatch.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.admin.patch.impl; + +import java.util.List; + +import org.alfresco.repo.admin.patch.AbstractPatch; +import org.alfresco.repo.domain.DbAccessControlEntry; +import org.alfresco.repo.domain.DbPermission; +import org.alfresco.repo.domain.hibernate.DbPermissionImpl; +import org.alfresco.service.namespace.QName; +import org.hibernate.Query; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.springframework.orm.hibernate3.HibernateCallback; +import org.springframework.orm.hibernate3.support.HibernateDaoSupport; + +/** + * Provides common functionality to change a permission type and/or name. + * + * @author Derek Hulley + */ +public abstract class AbstractPermissionChangePatch extends AbstractPatch +{ + private HibernateHelper helper; + + public AbstractPermissionChangePatch() + { + helper = new HibernateHelper(); + } + + public void setSessionFactory(SessionFactory sessionFactory) + { + this.helper.setSessionFactory(sessionFactory); + } + + /** + * Helper method to rename (move) a permission. This involves checking for the existence of the + * new permission and then moving all the entries to point to the new permission. + * + * @param oldTypeQName the old permission type + * @param oldName the old permission name + * @param newTypeQName the new permission type + * @param newName the new permission name + * @return Returns the number of permission entries modified + */ + protected int renamePermission(QName oldTypeQName, String oldName, QName newTypeQName, String newName) + { + return helper.createAndUpdatePermission(oldTypeQName, oldName, newTypeQName, newName); + } + + /** Helper to get a permission entity */ + private static class GetPermissionCallback implements HibernateCallback + { + private QName typeQName; + private String name; + public GetPermissionCallback(QName typeQName, String name) + { + this.typeQName = typeQName; + this.name = name; + } + public Object doInHibernate(Session session) + { + // flush any outstanding entities + session.flush(); + + Query query = session.getNamedQuery(HibernateHelper.QUERY_GET_PERMISSION); + query.setParameter("permissionTypeQName", typeQName) + .setString("permissionName", name); + return query.uniqueResult(); + } + } + + private static class HibernateHelper extends HibernateDaoSupport + { + private static final String QUERY_GET_PERMISSION = "permission.GetPermission"; + private static final String QUERY_GET_ENTRIES_TO_CHANGE = "permission.patch.GetAccessControlEntriesToChangePermissionOn"; + + public int createAndUpdatePermission( + final QName oldTypeQName, + final String oldName, + final QName newTypeQName, + final String newName) + { + if (oldTypeQName.equals(newTypeQName) && oldName.equals(newName)) + { + throw new IllegalArgumentException("Cannot move permission to itself: " + oldTypeQName + "-" + oldName); + } + + HibernateCallback getNewPermissionCallback = new GetPermissionCallback(newTypeQName, newName); + DbPermission permission = (DbPermission) getHibernateTemplate().execute(getNewPermissionCallback); + if (permission == null) + { + // create the permission + permission = new DbPermissionImpl(); + permission.setTypeQname(newTypeQName); + permission.setName(newName); + // save + getHibernateTemplate().save(permission); + } + final DbPermission newPermission = permission; + // now update all entries that refer to the old permission + HibernateCallback updateEntriesCallback = new HibernateCallback() + { + private static final int MAX_RESULTS = 1000; + @SuppressWarnings("unchecked") + public Object doInHibernate(Session session) + { + int count = 0; + while (true) + { + // flush any outstanding entities + session.flush(); + + Query query = session.getNamedQuery(HibernateHelper.QUERY_GET_ENTRIES_TO_CHANGE); + query.setParameter("oldTypeQName", oldTypeQName) + .setParameter("oldName", oldName) + .setMaxResults(MAX_RESULTS); + List entries = (List) query.list(); + // if there are no results, then we're done + if (entries.size() == 0) + { + break; + } + for (DbAccessControlEntry entry : entries) + { + entry.setPermission(newPermission); + count++; + session.evict(entry); + } + // flush and evict all the entries + session.flush(); + for (DbAccessControlEntry entry : entries) + { + session.evict(entry); + } + // next set of results + } + // done + return count; + } + }; + int updateCount = (Integer) getHibernateTemplate().execute(updateEntriesCallback); + // now delete the old permission + HibernateCallback getOldPermissionCallback = new GetPermissionCallback(oldTypeQName, oldName); + DbPermission oldPermission = (DbPermission) getHibernateTemplate().execute(getOldPermissionCallback); + if (oldPermission != null) + { + getHibernateTemplate().delete(oldPermission); + } + // done + return updateCount; + } + } +} diff --git a/source/java/org/alfresco/repo/admin/patch/impl/ActionRuleDecouplingPatch.java b/source/java/org/alfresco/repo/admin/patch/impl/ActionRuleDecouplingPatch.java index 8f4aab96c8..71d86005a3 100644 --- a/source/java/org/alfresco/repo/admin/patch/impl/ActionRuleDecouplingPatch.java +++ b/source/java/org/alfresco/repo/admin/patch/impl/ActionRuleDecouplingPatch.java @@ -54,6 +54,10 @@ public class ActionRuleDecouplingPatch extends AbstractPatch for (NodeRef origRuleNodeRef : resultSet.getNodeRefs()) { // Check that this rule need updated + if (!this.nodeService.exists(origRuleNodeRef)) + { + continue; + } Map origProperties = this.nodeService.getProperties(origRuleNodeRef); if (origProperties.containsKey(RuleModel.PROP_EXECUTE_ASYNC) == false) { @@ -79,21 +83,21 @@ public class ActionRuleDecouplingPatch extends AbstractPatch Map newProperties = this.nodeService.getProperties(newRuleNodeRef); // Set the rule type, execute async and applyToChildren properties on the rule - String ruleType = (String)origProperties.get(RuleModel.PROP_RULE_TYPE); + Serializable ruleType = origProperties.get(RuleModel.PROP_RULE_TYPE); origProperties.remove(RuleModel.PROP_RULE_TYPE); newProperties.put(RuleModel.PROP_RULE_TYPE, ruleType); - Boolean executeAsync = (Boolean)origProperties.get(ActionModel.PROP_EXECUTE_ASYNCHRONOUSLY); + Serializable executeAsync = origProperties.get(ActionModel.PROP_EXECUTE_ASYNCHRONOUSLY); origProperties.remove(ActionModel.PROP_EXECUTE_ASYNCHRONOUSLY); newProperties.put(RuleModel.PROP_EXECUTE_ASYNC, executeAsync); - Boolean applyToChildren = (Boolean)origProperties.get(RuleModel.PROP_APPLY_TO_CHILDREN); + Serializable applyToChildren = origProperties.get(RuleModel.PROP_APPLY_TO_CHILDREN); origProperties.remove(RuleModel.PROP_APPLY_TO_CHILDREN); newProperties.put(RuleModel.PROP_APPLY_TO_CHILDREN, applyToChildren); origProperties.remove(QName.createQName(RuleModel.RULE_MODEL_URI, "owningNodeRef")); // Move the action and description values from the composite action onto the rule - String title = (String)origProperties.get(ActionModel.PROP_ACTION_TITLE); + Serializable title = origProperties.get(ActionModel.PROP_ACTION_TITLE); origProperties.remove(ActionModel.PROP_ACTION_TITLE); - String description = (String)origProperties.get(ActionModel.PROP_ACTION_DESCRIPTION); + Serializable description = origProperties.get(ActionModel.PROP_ACTION_DESCRIPTION); origProperties.remove(ActionModel.PROP_ACTION_DESCRIPTION); newProperties.put(ContentModel.PROP_TITLE, title); newProperties.put(ContentModel.PROP_DESCRIPTION, description); diff --git a/source/java/org/alfresco/repo/admin/patch/impl/ContentPermissionPatch.java b/source/java/org/alfresco/repo/admin/patch/impl/ContentPermissionPatch.java index 6f428ccd92..031589d26c 100644 --- a/source/java/org/alfresco/repo/admin/patch/impl/ContentPermissionPatch.java +++ b/source/java/org/alfresco/repo/admin/patch/impl/ContentPermissionPatch.java @@ -16,33 +16,40 @@ */ package org.alfresco.repo.admin.patch.impl; -import org.alfresco.repo.admin.patch.AbstractPatch; -import org.alfresco.service.cmr.admin.PatchException; -import org.hibernate.SessionFactory; +import org.alfresco.i18n.I18NUtil; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; /** * Roles defined in permissionsDefinition.xml moved from cm:content to sys:base. - * This effects the data stored in the node_perm_entry table. - *

    - * WILL NOT EXECUTE ANYMORE + * This effects the data stored in the permission table. * * @author Derek Hulley */ -public class ContentPermissionPatch extends AbstractPatch +public class ContentPermissionPatch extends AbstractPermissionChangePatch { - private static final String MSG_UPGRADE = "patch.contentPermission.upgrade"; - - public ContentPermissionPatch() - { - } - - public void setSessionFactory(SessionFactory sessionFactory) - { - } + private static final String MSG_SUCCESS = "patch.contentPermission.result"; + private static final QName TYPE_QNAME_OLD = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "content"); + private static final QName TYPE_QNAME_NEW = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "base"); + private static final String[] NAMES = new String[] {"Execute", "ReadContent", "WriteContent", "ExecuteContent"}; + @Override protected String applyInternal() throws Exception { - throw new PatchException(MSG_UPGRADE); + int updateCount = 0; + for (String permissionName : NAMES) + { + updateCount += super.renamePermission( + ContentPermissionPatch.TYPE_QNAME_OLD, + permissionName, + ContentPermissionPatch.TYPE_QNAME_NEW, + permissionName); + } + + // build the result message + String msg = I18NUtil.getMessage(MSG_SUCCESS, updateCount); + // done + return msg; } } diff --git a/source/java/org/alfresco/repo/admin/patch/impl/PermissionDataPatch.java b/source/java/org/alfresco/repo/admin/patch/impl/PermissionDataPatch.java index 8c815a7d1f..5d18fb4549 100644 --- a/source/java/org/alfresco/repo/admin/patch/impl/PermissionDataPatch.java +++ b/source/java/org/alfresco/repo/admin/patch/impl/PermissionDataPatch.java @@ -16,35 +16,42 @@ */ package org.alfresco.repo.admin.patch.impl; -import org.alfresco.repo.admin.patch.AbstractPatch; -import org.alfresco.service.cmr.admin.PatchException; -import org.hibernate.SessionFactory; +import org.alfresco.i18n.I18NUtil; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; /** * The roles defined in permissionsDefinition.xml moved from cm:folder to cm:cmobject. - * This effects the data stored in the node_perm_entry table. + * This effects the data stored in the permission table. *

    * JIRA: {@link http://www.alfresco.org/jira/browse/AR-344 AR-344} - *

    - * WILL NOT EXECUTE ANYMORE * * @author Derek Hulley */ -public class PermissionDataPatch extends AbstractPatch +public class PermissionDataPatch extends AbstractPermissionChangePatch { - private static final String MSG_UPGRADE = "patch.updatePermissionData.upgrade"; - - public PermissionDataPatch() - { - } - - public void setSessionFactory(SessionFactory sessionFactory) - { - } + private static final String MSG_SUCCESS = "patch.updatePermissionData.result"; + private static final QName TYPE_QNAME_OLD = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "folder"); + private static final QName TYPE_QNAME_NEW = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "cmobject"); + private static final String[] NAMES = new String[] {"Coordinator", "Contributor", "Editor", "Guest"}; + @Override protected String applyInternal() throws Exception { - throw new PatchException(MSG_UPGRADE); + int updateCount = 0; + for (String permissionName : NAMES) + { + updateCount += super.renamePermission( + PermissionDataPatch.TYPE_QNAME_OLD, + permissionName, + PermissionDataPatch.TYPE_QNAME_NEW, + permissionName); + } + + // build the result message + String msg = I18NUtil.getMessage(MSG_SUCCESS, updateCount); + // done + return msg; } } diff --git a/source/java/org/alfresco/repo/admin/patch/impl/UpdateGuestPermissionPatch.java b/source/java/org/alfresco/repo/admin/patch/impl/UpdateGuestPermissionPatch.java index 58fd936812..374a393835 100644 --- a/source/java/org/alfresco/repo/admin/patch/impl/UpdateGuestPermissionPatch.java +++ b/source/java/org/alfresco/repo/admin/patch/impl/UpdateGuestPermissionPatch.java @@ -16,32 +16,27 @@ */ package org.alfresco.repo.admin.patch.impl; -import org.alfresco.repo.admin.patch.AbstractPatch; -import org.alfresco.service.cmr.admin.PatchException; -import org.hibernate.SessionFactory; +import org.alfresco.i18n.I18NUtil; +import org.alfresco.model.ContentModel; /** * The permission 'Guest' has been renamed to 'Consumer'. - *

    - * WILL NOT EXECUTE ANYMORE * * @author David Caruana + * @author Derek Hulley */ -public class UpdateGuestPermissionPatch extends AbstractPatch +public class UpdateGuestPermissionPatch extends AbstractPermissionChangePatch { - private static final String MSG_UPGRADE = "patch.updateGuestPermission.upgrade"; - - public UpdateGuestPermissionPatch() - { - } - - public void setSessionFactory(SessionFactory sessionFactory) - { - } + private static final String MSG_SUCCESS = "patch.updateGuestPermission.result"; @Override protected String applyInternal() throws Exception { - throw new PatchException(MSG_UPGRADE); + int updateCount = super.renamePermission(ContentModel.TYPE_CMOBJECT, "Guest", ContentModel.TYPE_CMOBJECT, "Consumer"); + + // build the result message + String msg = I18NUtil.getMessage(MSG_SUCCESS, updateCount); + // done + return msg; } } diff --git a/source/java/org/alfresco/repo/admin/patch/util/ImportFileUpdater.java b/source/java/org/alfresco/repo/admin/patch/util/ImportFileUpdater.java new file mode 100644 index 0000000000..4148f74de8 --- /dev/null +++ b/source/java/org/alfresco/repo/admin/patch/util/ImportFileUpdater.java @@ -0,0 +1,573 @@ +/** + * + */ +package org.alfresco.repo.admin.patch.util; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.ActionModel; +import org.alfresco.repo.rule.RuleModel; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.util.GUID; +import org.dom4j.io.OutputFormat; +import org.dom4j.io.XMLWriter; +import org.xml.sax.helpers.AttributesImpl; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +/** + * Updates a XML import file to be compatable with the current version of the repository. + * + * @author royw + */ +public class ImportFileUpdater +{ + /** Indent size **/ + private static int INDENT_SIZE = 2; + + /** The destination export version number **/ + private static String EXPORT_VERSION = "1.4.0"; + + /** Element names **/ + private static String NAME_EXPORTER_VERSION = "exporterVersion"; + private static String NAME_RULE = "rule"; + + /** The current import version number **/ + private String version; + + /** + * Updates the passed import file into the equivalent 1.4 format. + * + * @param source the source import file + * @param destination the destination import file + */ + public void updateImportFile(String source, String destination) + { + XmlPullParser reader = getReader(source); + XMLWriter writer = getWriter(destination); + + try + { + // Start the documentation + writer.startDocument(); + + // Start reading the document + int eventType = reader.getEventType(); + while (eventType != XmlPullParser.END_DOCUMENT) + { + if (eventType == XmlPullParser.START_TAG) + { + ImportFileUpdater.this.outputCurrentElement(reader, writer, new OutputChildren()); + } + eventType = reader.next(); + } + + // End and close the document + writer.endDocument(); + writer.close(); + } + catch (Exception exception) + { + throw new AlfrescoRuntimeException("Unable to update import file.", exception); + } + + } + + /** + * Get the reader for the source import file + * + * @param source the source import file + * @return the XML pull parser used to read the file + */ + private XmlPullParser getReader(String source) + { + try + { + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(System.getProperty(XmlPullParserFactory.PROPERTY_NAME), null); + factory.setNamespaceAware(true); + + XmlPullParser xpp = factory.newPullParser(); + xpp.setInput(new FileReader(source)); + + return xpp; + } + catch (XmlPullParserException exception) + { + throw new AlfrescoRuntimeException("Unable to update import file.", exception); + } + catch (FileNotFoundException fileNotFound) + { + throw new AlfrescoRuntimeException("The source file could not be loaded.", fileNotFound); + } + } + + /** + * Get the writer for the import file + * + * @param destination the destination XML import file + * @return the XML writer + */ + private XMLWriter getWriter(String destination) + { + try + { + // Define output format + OutputFormat format = OutputFormat.createPrettyPrint(); + format.setNewLineAfterDeclaration(false); + format.setIndentSize(INDENT_SIZE); + format.setEncoding("UTF-8"); + + return new XMLWriter(new FileOutputStream(destination), format); + } + catch (Exception exception) + { + throw new AlfrescoRuntimeException("Unable to create XML writer.", exception); + } + } + + private void outputCurrentElement(XmlPullParser reader, XMLWriter writer, Work work) + throws Exception + { + outputCurrentElement(reader, writer, work, true); + } + + private void outputCurrentElement(XmlPullParser reader, XMLWriter writer, Work work, boolean checkForCallbacks) + throws Exception + { + if (checkForCallbacks == false || checkForCallbacks(reader, writer) == false) + { + // Get the name details of the element + String name = reader.getName(); + String namespace = reader.getNamespace(); + String prefix = reader.getPrefix(); + + // Sort out namespaces + Map nss = new HashMap(); + int nsStart = reader.getNamespaceCount(reader.getDepth()-1); + int nsEnd = reader.getNamespaceCount(reader.getDepth()); + for (int i = nsStart; i < nsEnd; i++) + { + String nsPrefix = reader.getNamespacePrefix(i); + String ns = reader.getNamespaceUri(i); + nss.put(nsPrefix, ns); + } + + // Sort out attributes + AttributesImpl attributes = new AttributesImpl(); + for (int i = 0; i < reader.getAttributeCount(); i++) + { + String attributeName = reader.getAttributeName(i); + String attributeNamespace = reader.getAttributeNamespace(i); + String attributePrefix = reader.getAttributePrefix(i); + String attributeType = reader.getAttributeType(i); + String attributeValue = reader.getAttributeValue(i); + + attributes.addAttribute(attributeNamespace, attributeName, attributePrefix+":"+attributeName, attributeType, attributeValue); + } + + // Start the namespace prefixes + for (Map.Entry entry : nss.entrySet()) + { + writer.startPrefixMapping(entry.getKey(), entry.getValue()); + } + + // Write the start of the element + writer.startElement(namespace, name, prefix+":"+name, attributes); + + // Do the work + work.doWork(reader, writer); + + // Write the end of the element + writer.endElement(namespace, name, prefix+":"+name); + + // End the namespace prefixes + for (String nsPrefix : nss.keySet()) + { + writer.endPrefixMapping(nsPrefix); + } + } + } + + private boolean checkForCallbacks(XmlPullParser reader, XMLWriter writer) + throws Exception + { + boolean result = false; + if (reader.getName().equals(NAME_EXPORTER_VERSION) == true) + { + new ImportVersionLabelCallback().doCallback(reader, writer); + result = true; + } + else if (reader.getName().equals(NAME_RULE) == true) + { + if (this.version.startsWith("1.3") == true) + { + new RuleCallback().doCallback(reader, writer); + result = true; + } + } + return result; + } + + private interface Work + { + void doWork(XmlPullParser reader, XMLWriter writer) + throws Exception; + } + + private class OutputChildren implements Work + { + public void doWork(XmlPullParser reader, XMLWriter writer) + throws Exception + { + // Deal with the contents of the tag + int eventType = reader.getEventType(); + while (eventType != XmlPullParser.END_TAG) + { + eventType = reader.next(); + if (eventType == XmlPullParser.START_TAG) + { + ImportFileUpdater.this.outputCurrentElement(reader, writer, new OutputChildren()); + } + else if (eventType == XmlPullParser.TEXT) + { + // Write the text to the output file + writer.write(reader.getText()); + } + } + } + } + + @SuppressWarnings("unused") + private class IgnoreChildren implements Work + { + public void doWork(XmlPullParser reader, XMLWriter writer) + throws Exception + { + int eventType = reader.getEventType(); + while (eventType != XmlPullParser.END_TAG) + { + eventType = reader.next(); + if (eventType == XmlPullParser.START_TAG) + { + doWork(reader, writer); + } + } + } + } + + private interface ImportUpdaterCallback + { + void doCallback(XmlPullParser reader, XMLWriter writer) + throws Exception; + } + + private class ImportVersionLabelCallback implements ImportUpdaterCallback + { + public void doCallback(XmlPullParser reader, XMLWriter writer) + throws Exception + { + ImportFileUpdater.this.outputCurrentElement(reader, writer, + new Work() + { + public void doWork(XmlPullParser reader, XMLWriter writer) throws Exception + { + reader.next(); + ImportFileUpdater.this.version = reader.getText(); + writer.write(EXPORT_VERSION); + reader.next(); + } + }, false); + } + } + + private class RuleCallback implements ImportUpdaterCallback + { + public void doCallback(XmlPullParser reader, XMLWriter writer) + throws Exception + { + // Get the name details of the element + String name = reader.getName(); + String namespace = reader.getNamespace(); + String prefix = reader.getPrefix(); + + // Rename the child assoc appropriately + AttributesImpl attributes = new AttributesImpl(); + String attributeName = reader.getAttributeName(0); + String attributeNamespace = reader.getAttributeNamespace(0); + String attributePrefix = reader.getAttributePrefix(0); + String attributeType = reader.getAttributeType(0); + String attributeValue = reader.getAttributeValue(0) + GUID.generate(); + attributes.addAttribute(attributeNamespace, attributeName, attributePrefix+":"+attributeName, attributeType, attributeValue); + + // Output the rules element + writer.startElement(namespace, name, prefix+":"+name, attributes); + + int eventType = reader.getEventType(); + while (eventType != XmlPullParser.END_TAG) + { + eventType = reader.next(); + if (eventType == XmlPullParser.START_TAG) + { + String childName = reader.getName(); + if (childName.equals("aspects") == true) + { + ImportFileUpdater.this.outputCurrentElement(reader, writer, + new Work() + { + public void doWork(XmlPullParser reader, XMLWriter writer) throws Exception + { + // Add titled aspect + writer.startElement( + ContentModel.ASPECT_TITLED.getNamespaceURI(), + ContentModel.ASPECT_TITLED.getLocalName(), + NamespaceService.CONTENT_MODEL_PREFIX + ":" + ContentModel.ASPECT_TITLED.getLocalName(), + new AttributesImpl()); + writer.endElement( + ContentModel.ASPECT_TITLED.getNamespaceURI(), + ContentModel.ASPECT_TITLED.getLocalName(), + NamespaceService.CONTENT_MODEL_PREFIX + ":" + ContentModel.ASPECT_TITLED.getLocalName()); + + // Read the rest of the elements and output + int eventType = reader.getEventType(); + while (eventType != XmlPullParser.END_TAG) + { + eventType = reader.next(); + if (eventType == XmlPullParser.START_TAG) + { + ImportFileUpdater.this.outputCurrentElement(reader, writer, new OutputChildren()); + } + } + } + + }, false); + } + else if (childName.equals("properties") == true) + { + ImportFileUpdater.this.outputCurrentElement(reader, writer, + new Work() + { + public void doWork(XmlPullParser reader, XMLWriter writer) throws Exception + { + int eventType = reader.getEventType(); + while (eventType != XmlPullParser.END_TAG) + { + eventType = reader.next(); + if (eventType == XmlPullParser.START_TAG) + { + String propName = reader.getName(); + if (propName.equals("actionDescription") == true) + { + writer.startElement( + ContentModel.PROP_DESCRIPTION.getNamespaceURI(), + ContentModel.PROP_DESCRIPTION.getLocalName(), + NamespaceService.CONTENT_MODEL_PREFIX + ":" + ContentModel.PROP_DESCRIPTION.getLocalName(), + new AttributesImpl()); + + // Output the value within + new OutputChildren().doWork(reader, writer); + + writer.endElement( + ContentModel.PROP_DESCRIPTION.getNamespaceURI(), + ContentModel.PROP_DESCRIPTION.getLocalName(), + NamespaceService.CONTENT_MODEL_PREFIX + ":" + ContentModel.PROP_DESCRIPTION.getLocalName()); + eventType = reader.next(); + + } + else if (propName.equals("actionTitle") == true) + { + writer.startElement( + ContentModel.PROP_TITLE.getNamespaceURI(), + ContentModel.PROP_TITLE.getLocalName(), + NamespaceService.CONTENT_MODEL_PREFIX + ":" + ContentModel.PROP_TITLE.getLocalName(), + new AttributesImpl()); + + // Output the value within + new OutputChildren().doWork(reader, writer); + + writer.endElement( + ContentModel.PROP_TITLE.getNamespaceURI(), + ContentModel.PROP_TITLE.getLocalName(), + NamespaceService.CONTENT_MODEL_PREFIX + ":" + ContentModel.PROP_TITLE.getLocalName()); + eventType = reader.next(); + } + else if (propName.equals("executeAsynchronously") == true) + { + writer.startElement( + RuleModel.PROP_EXECUTE_ASYNC.getNamespaceURI(), + RuleModel.PROP_EXECUTE_ASYNC.getLocalName(), + RuleModel.RULE_MODEL_PREFIX + ":" + RuleModel.PROP_EXECUTE_ASYNC.getLocalName(), + new AttributesImpl()); + + // Output the value within + new OutputChildren().doWork(reader, writer); + + writer.endElement( + RuleModel.PROP_EXECUTE_ASYNC.getNamespaceURI(), + RuleModel.PROP_EXECUTE_ASYNC.getLocalName(), + RuleModel.RULE_MODEL_PREFIX + ":" + RuleModel.PROP_EXECUTE_ASYNC.getLocalName()); + eventType = reader.next(); + } + else if (propName.equals("ruleType") == true) + { + ImportFileUpdater.this.outputCurrentElement(reader, writer, + new Work() + { + public void doWork(XmlPullParser reader, XMLWriter writer) throws Exception + { + // Output the elements that contain a multi values property + writer.startElement(NamespaceService.REPOSITORY_VIEW_1_0_URI, "values", "view:values", new AttributesImpl()); + writer.startElement(NamespaceService.REPOSITORY_VIEW_1_0_URI, "value", "view:value", new AttributesImpl()); + + // Output the value within + new OutputChildren().doWork(reader, writer); + + // End the multi values elements + writer.endElement(NamespaceService.REPOSITORY_VIEW_PREFIX, "value", "view:value"); + writer.endElement(NamespaceService.REPOSITORY_VIEW_PREFIX, "values", "view:values"); + } + }, false); + } + else if (propName.equals("definitionName") == true) + { + // Skip past next end + while (eventType != XmlPullParser.END_TAG) + { + eventType = reader.next(); + } + eventType = reader.next(); + } + else + { + ImportFileUpdater.this.outputCurrentElement(reader, writer, new OutputChildren()); + } + } + } + + // Output value for the disabled property + writer.startElement( + RuleModel.PROP_DISABLED.getNamespaceURI(), + RuleModel.PROP_DISABLED.getLocalName(), + RuleModel.RULE_MODEL_PREFIX + ":" + RuleModel.PROP_DISABLED.getLocalName(), + new AttributesImpl()); + writer.write("false"); + writer.endElement( + RuleModel.PROP_DISABLED.getNamespaceURI(), + RuleModel.PROP_DISABLED.getLocalName(), + RuleModel.RULE_MODEL_PREFIX + ":" + RuleModel.PROP_DISABLED.getLocalName()); + } + }, false); + } + else if (childName.equals("associations") == true) + { + ImportFileUpdater.this.outputCurrentElement(reader, writer, + new Work() + { + public void doWork(XmlPullParser reader, XMLWriter writer) throws Exception + { + // + writer.startElement( + RuleModel.ASSOC_ACTION.getNamespaceURI(), + RuleModel.ASSOC_ACTION.getLocalName(), + RuleModel.RULE_MODEL_PREFIX + ":" + RuleModel.ASSOC_ACTION.getLocalName(), + new AttributesImpl()); + + // + AttributesImpl attributes = new AttributesImpl(); + attributes.addAttribute(NamespaceService.REPOSITORY_VIEW_1_0_URI, "childName", "view:childName", null, "rule:action"); + writer.startElement( + ActionModel.TYPE_COMPOSITE_ACTION.getNamespaceURI(), + ActionModel.TYPE_COMPOSITE_ACTION.getLocalName(), + ActionModel.ACTION_MODEL_PREFIX+ ":" + ActionModel.TYPE_COMPOSITE_ACTION.getLocalName(), + attributes); + + // + writer.startElement( + NamespaceService.REPOSITORY_VIEW_1_0_URI, + "properties", + "view:properties", + new AttributesImpl()); + + // composite-action + writer.startElement( + ActionModel.PROP_DEFINITION_NAME.getNamespaceURI(), + ActionModel.PROP_DEFINITION_NAME.getLocalName(), + ActionModel.ACTION_MODEL_PREFIX + ":" + ActionModel.PROP_DEFINITION_NAME.getLocalName(), + new AttributesImpl()); + writer.write("composite-action"); + writer.endElement( + ActionModel.PROP_DEFINITION_NAME.getNamespaceURI(), + ActionModel.PROP_DEFINITION_NAME.getLocalName(), + ActionModel.ACTION_MODEL_PREFIX + ":" + ActionModel.PROP_DEFINITION_NAME.getLocalName()); + + // + writer.endElement( + NamespaceService.REPOSITORY_VIEW_1_0_URI, + "properties", + "view:properties"); + + // + writer.startElement( + NamespaceService.REPOSITORY_VIEW_1_0_URI, + "associations", + "view:associations", + new AttributesImpl()); + + // Output the association details + new OutputChildren().doWork(reader, writer); + + // + writer.endElement( + NamespaceService.REPOSITORY_VIEW_1_0_URI, + "associations", + "view:associations"); + + // + writer.endElement( + ActionModel.TYPE_COMPOSITE_ACTION.getNamespaceURI(), + ActionModel.TYPE_COMPOSITE_ACTION.getLocalName(), + ActionModel.ACTION_MODEL_PREFIX+ ":" + ActionModel.TYPE_COMPOSITE_ACTION.getLocalName()); + + // + writer.endElement( + RuleModel.ASSOC_ACTION.getNamespaceURI(), + RuleModel.ASSOC_ACTION.getLocalName(), + RuleModel.RULE_MODEL_PREFIX + ":" + RuleModel.ASSOC_ACTION.getLocalName()); + } + }, false); + } + else + { + // Output anything else that might be hanging araound + ImportFileUpdater.this.outputCurrentElement(reader, writer, new OutputChildren()); + } + } + } + + // End the rules element + writer.endElement(namespace, name, prefix+":"+name); + } + } + + public static void main(String[] args) + { + if (args.length == 2) + { + ImportFileUpdater util = new ImportFileUpdater(); + util.updateImportFile(args[0], args[1]); + } + else + { + System.out.println(" ImportFileUpdater "); + System.out.println(" source - 1.3 import file name to be updated"); + System.out.println(" destination - name of the generated 1.4 import file"); + } + } + +} diff --git a/source/java/org/alfresco/repo/copy/CopyServiceImpl.java b/source/java/org/alfresco/repo/copy/CopyServiceImpl.java index d9a0f7e59e..34e5436456 100644 --- a/source/java/org/alfresco/repo/copy/CopyServiceImpl.java +++ b/source/java/org/alfresco/repo/copy/CopyServiceImpl.java @@ -25,6 +25,7 @@ import java.util.Map; import java.util.Set; import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.i18n.I18NUtil; import org.alfresco.model.ContentModel; import org.alfresco.repo.policy.ClassPolicyDelegate; import org.alfresco.repo.policy.JavaBehaviour; @@ -43,6 +44,7 @@ import org.alfresco.service.cmr.repository.AssociationRef; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.CopyService; import org.alfresco.service.cmr.repository.CopyServiceException; +import org.alfresco.service.cmr.repository.DuplicateChildNodeNameException; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.StoreRef; @@ -65,8 +67,12 @@ import org.alfresco.util.ParameterCheck; */ public class CopyServiceImpl implements CopyService { + /** I18N labels */ + private String COPY_OF_LABEL = "copy_service.copy_of_label"; + /** The node service */ private NodeService nodeService; + private NodeService internalNodeService; /** The dictionary service*/ private DictionaryService dictionaryService; @@ -99,6 +105,16 @@ public class CopyServiceImpl implements CopyService { this.nodeService = nodeService; } + + /** + * Sets the internal node service + * + * @param internalNodeService the internal node service + */ + public void setInternalNodeService(NodeService internalNodeService) + { + this.internalNodeService = internalNodeService; + } /** * Sets the dictionary service @@ -233,7 +249,32 @@ public class CopyServiceImpl implements CopyService return copy; } - + + public NodeRef copyAndRename(NodeRef sourceNodeRef, NodeRef destinationParent, QName destinationAssocTypeQName, QName destinationQName, boolean copyChildren) + { + // Make a note of the source name and do the copy + String sourceName = (String)this.internalNodeService.getProperty(sourceNodeRef, ContentModel.PROP_NAME); + NodeRef copy = copy(sourceNodeRef, destinationParent, destinationAssocTypeQName, destinationQName, copyChildren); + + // Do the rename, iterating until a non-duplicate name is found + boolean bDone = false; + while (bDone == false) + { + try + { + this.internalNodeService.setProperty(copy, ContentModel.PROP_NAME, sourceName); + bDone = true; + } + catch(DuplicateChildNodeNameException exception) + { + sourceName = I18NUtil.getMessage(COPY_OF_LABEL, sourceName); + } + } + + // Return the copy + return copy; + } + /** * Invokes the copy complete policy for the node reference provided * diff --git a/source/java/org/alfresco/repo/copy/CopyServiceImplTest.java b/source/java/org/alfresco/repo/copy/CopyServiceImplTest.java index 973ff0c827..426d157706 100644 --- a/source/java/org/alfresco/repo/copy/CopyServiceImplTest.java +++ b/source/java/org/alfresco/repo/copy/CopyServiceImplTest.java @@ -104,6 +104,7 @@ public class CopyServiceImplTest extends BaseSpringTest private static final QName TEST_MANDATORY_ASPECT_QNAME = QName.createQName(TEST_TYPE_NAMESPACE, "testMandatoryAspect"); private static final QName PROP5_QNAME_MANDATORY = QName.createQName(TEST_TYPE_NAMESPACE, "prop5Mandatory"); + private static final String TEST_NAME = "testName"; private static final String TEST_VALUE_1 = "testValue1"; private static final String TEST_VALUE_2 = "testValue2"; private static final String TEST_VALUE_3 = "testValue3"; @@ -239,6 +240,7 @@ public class CopyServiceImplTest extends BaseSpringTest private Map createTypePropertyBag() { Map result = new HashMap(); + result.put(ContentModel.PROP_NAME, TEST_NAME); result.put(PROP1_QNAME_MANDATORY, TEST_VALUE_1); result.put(PROP2_QNAME_OPTIONAL, TEST_VALUE_2); result.put(PROP5_QNAME_MANDATORY, TEST_VALUE_3); @@ -624,6 +626,31 @@ public class CopyServiceImplTest extends BaseSpringTest assertNotNull(value); assertEquals(nodeTwoCopy, value); } + + public void testCopyAndRename() + { + // Check a normal copy with no dup restrictions + NodeRef copy = this.copyService.copyAndRename( + this.sourceNodeRef, + this.rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName("{test}copyAssoc"), + false); + checkCopiedNode(this.sourceNodeRef, copy, true, true, false); + assertTrue(TEST_NAME.equals(this.nodeService.getProperty(copy, ContentModel.PROP_NAME))); + + // Create a folder and content node + Map propsFolder = new HashMap(1); + propsFolder.put(ContentModel.PROP_NAME, "tempFolder"); + NodeRef folderNode = this.nodeService.createNode(this.rootNodeRef, ContentModel.ASSOC_CHILDREN, QName.createQName("{test}tempFolder"), ContentModel.TYPE_FOLDER, propsFolder).getChildRef(); + Map props = new HashMap(1); + props.put(ContentModel.PROP_NAME, TEST_NAME); + NodeRef contentNode = this.nodeService.createNode(folderNode, ContentModel.ASSOC_CONTAINS, QName.createQName("{test}renametest"), ContentModel.TYPE_CONTENT, props).getChildRef(); + + // Now copy the content node with the duplicate name restriction + NodeRef contentCopy = this.copyService.copy(contentNode, folderNode, ContentModel.ASSOC_CONTAINS, QName.createQName("{test}bobbins"), false); + assertFalse(TEST_NAME.equals(this.nodeService.getProperty(contentCopy, ContentModel.PROP_NAME))); + } /** * Check that the copied node contains the state we are expecting diff --git a/source/java/org/alfresco/repo/descriptor/DescriptorServiceImpl.java b/source/java/org/alfresco/repo/descriptor/DescriptorServiceImpl.java index 158b3fae5b..fc813f1bc4 100644 --- a/source/java/org/alfresco/repo/descriptor/DescriptorServiceImpl.java +++ b/source/java/org/alfresco/repo/descriptor/DescriptorServiceImpl.java @@ -41,16 +41,12 @@ import org.alfresco.service.license.LicenseService; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.AbstractLifecycleBean; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationEvent; -import org.springframework.context.ApplicationListener; -import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.core.io.Resource; @@ -59,12 +55,10 @@ import org.springframework.core.io.Resource; * * @author David Caruana */ -public class DescriptorServiceImpl implements DescriptorService, ApplicationListener, InitializingBean, ApplicationContextAware, DisposableBean +public class DescriptorServiceImpl extends AbstractLifecycleBean implements DescriptorService, InitializingBean { private static Log logger = LogFactory.getLog(DescriptorServiceImpl.class); - private ApplicationContext applicationContext; - private Properties serverProperties; private ImporterBootstrap systemBootstrap; @@ -78,14 +72,6 @@ public class DescriptorServiceImpl implements DescriptorService, ApplicationList private Descriptor installedRepoDescriptor; - /* (non-Javadoc) - * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) - */ - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException - { - this.applicationContext = applicationContext; - } - /** * Sets the server descriptor from a resource file * @@ -163,36 +149,36 @@ public class DescriptorServiceImpl implements DescriptorService, ApplicationList return (licenseService == null) ? null : licenseService.getLicense(); } - /** - * @param event - */ - public void onApplicationEvent(ApplicationEvent event) + @Override + protected void onBootstrap(ApplicationEvent event) { - if (event instanceof ContextRefreshedEvent) + // initialise the repository descriptor + // note: this requires that the repository schema has already been initialised + TransactionWork createDescriptorWork = new TransactionUtil.TransactionWork() { - // initialise the repository descriptor - // note: this requires that the repository schema has already been initialised - TransactionWork createDescriptorWork = new TransactionUtil.TransactionWork() + public Descriptor doWork() { - public Descriptor doWork() - { - // initialise license service (if installed) - initialiseLicenseService(); - - // verify license, but only if license component is installed - licenseService.verifyLicense(); - - // persist the server descriptor values - updateCurrentRepositoryDescriptor(serverDescriptor); + // initialise license service (if installed) + initialiseLicenseService(); + + // verify license, but only if license component is installed + licenseService.verifyLicense(); + + // persist the server descriptor values + updateCurrentRepositoryDescriptor(serverDescriptor); - // return the repository installed descriptor - return createInstalledRepositoryDescriptor(); - } - }; - installedRepoDescriptor = TransactionUtil.executeInUserTransaction(transactionService, createDescriptorWork); - } + // return the repository installed descriptor + return createInstalledRepositoryDescriptor(); + } + }; + installedRepoDescriptor = TransactionUtil.executeInUserTransaction(transactionService, createDescriptorWork); } + @Override + protected void onShutdown(ApplicationEvent event) + { + } + /** * Initialise Descriptors */ @@ -202,13 +188,6 @@ public class DescriptorServiceImpl implements DescriptorService, ApplicationList serverDescriptor = createServerDescriptor(); } - /** - * Destruction hook - */ - public void destroy() throws Exception - { - } - /** * Create server descriptor * @@ -358,7 +337,7 @@ public class DescriptorServiceImpl implements DescriptorService, ApplicationList // be declaratively taken out in an installed environment. Class licenseComponentClass = Class.forName("org.alfresco.license.LicenseComponent"); Constructor constructor = licenseComponentClass.getConstructor(new Class[] { ApplicationContext.class} ); - licenseService = (LicenseService)constructor.newInstance(new Object[] { applicationContext }); + licenseService = (LicenseService)constructor.newInstance(new Object[] { getApplicationContext() }); } catch (ClassNotFoundException e) { @@ -766,4 +745,5 @@ public class DescriptorServiceImpl implements DescriptorService, ApplicationList return serverProperties.getProperty(key, ""); } } + } diff --git a/source/java/org/alfresco/repo/descriptor/DescriptorStartupLog.java b/source/java/org/alfresco/repo/descriptor/DescriptorStartupLog.java index 13d77fff3b..bd802737cc 100644 --- a/source/java/org/alfresco/repo/descriptor/DescriptorStartupLog.java +++ b/source/java/org/alfresco/repo/descriptor/DescriptorStartupLog.java @@ -23,11 +23,10 @@ import java.util.Map; import org.alfresco.service.descriptor.Descriptor; import org.alfresco.service.descriptor.DescriptorService; import org.alfresco.service.license.LicenseDescriptor; +import org.alfresco.util.AbstractLifecycleBean; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationEvent; -import org.springframework.context.ApplicationListener; -import org.springframework.context.event.ContextRefreshedEvent; /** @@ -35,7 +34,7 @@ import org.springframework.context.event.ContextRefreshedEvent; * * @author davidc */ -public class DescriptorStartupLog implements ApplicationListener +public class DescriptorStartupLog extends AbstractLifecycleBean { // Logger private static final Log logger = LogFactory.getLog(DescriptorService.class); @@ -52,82 +51,6 @@ public class DescriptorStartupLog implements ApplicationListener } - /** - * @param event - */ - public void onApplicationEvent(ApplicationEvent event) - { - if (event instanceof ContextRefreshedEvent) - { - // - // log output of VM stats - // - Map properties = System.getProperties(); - String version = (properties.get("java.runtime.version") == null) ? "unknown" : (String)properties.get("java.runtime.version"); - long maxHeap = Runtime.getRuntime().maxMemory(); - float maxHeapMB = maxHeap / 1024l; - maxHeapMB = maxHeapMB / 1024l; - if (logger.isInfoEnabled()) - { - logger.info(String.format("Alfresco JVM - v%s; maximum heap size %.3fMB", version, maxHeapMB)); - } - if (logger.isWarnEnabled()) - { - if (version.startsWith("1.2") || version.startsWith("1.3") || version.startsWith("1.4")) - { - logger.warn(String.format("Alfresco JVM - WARNING - v1.5 is required; currently using v%s", version)); - } - if (maxHeapMB < 500) - { - logger.warn(String.format("Alfresco JVM - WARNING - maximum heap size %.3fMB is less than recommended 512MB", maxHeapMB)); - } - } - - // Log License Descriptors (if applicable) - LicenseDescriptor license = descriptorService.getLicenseDescriptor(); - if (license != null && logger.isInfoEnabled()) - { - String subject = license.getSubject(); - String msg = "Alfresco license: " + subject; - String holder = getHolderOrganisation(license.getHolder()); - if (holder != null) - { - msg += " granted to " + holder; - } - Date validUntil = license.getValidUntil(); - if (validUntil != null) - { - Integer days = license.getDays(); - Integer remainingDays = license.getRemainingDays(); - - msg += " limited to " + days + " days expiring " + validUntil + " (" + remainingDays + " days remaining)"; - } - else - { - msg += " (does not expire)"; - } - - - logger.info(msg); - } - - // Log Repository Descriptors - if (logger.isInfoEnabled()) - { - Descriptor serverDescriptor = descriptorService.getServerDescriptor(); - Descriptor installedRepoDescriptor = descriptorService.getInstalledRepositoryDescriptor(); - String serverEdition = serverDescriptor.getEdition(); - String serverVersion = serverDescriptor.getVersion(); - int serverSchemaVersion = serverDescriptor.getSchema(); - String installedRepoVersion = installedRepoDescriptor.getVersion(); - int installedSchemaVersion = installedRepoDescriptor.getSchema(); - logger.info(String.format("Alfresco started (%s): Current version %s schema %d - Installed version %s schema %d", - serverEdition, serverVersion, serverSchemaVersion, installedRepoVersion, installedSchemaVersion)); - } - } - } - - /** * Get Organisation from Principal * @@ -156,5 +79,83 @@ public class DescriptorStartupLog implements ApplicationListener return holder; } + + + @Override + protected void onBootstrap(ApplicationEvent event) + { + // + // log output of VM stats + // + Map properties = System.getProperties(); + String version = (properties.get("java.runtime.version") == null) ? "unknown" : (String)properties.get("java.runtime.version"); + long maxHeap = Runtime.getRuntime().maxMemory(); + float maxHeapMB = maxHeap / 1024l; + maxHeapMB = maxHeapMB / 1024l; + if (logger.isInfoEnabled()) + { + logger.info(String.format("Alfresco JVM - v%s; maximum heap size %.3fMB", version, maxHeapMB)); + } + if (logger.isWarnEnabled()) + { + if (version.startsWith("1.2") || version.startsWith("1.3") || version.startsWith("1.4")) + { + logger.warn(String.format("Alfresco JVM - WARNING - v1.5 is required; currently using v%s", version)); + } + if (maxHeapMB < 500) + { + logger.warn(String.format("Alfresco JVM - WARNING - maximum heap size %.3fMB is less than recommended 512MB", maxHeapMB)); + } + } + + // Log License Descriptors (if applicable) + LicenseDescriptor license = descriptorService.getLicenseDescriptor(); + if (license != null && logger.isInfoEnabled()) + { + String subject = license.getSubject(); + String msg = "Alfresco license: " + subject; + String holder = getHolderOrganisation(license.getHolder()); + if (holder != null) + { + msg += " granted to " + holder; + } + Date validUntil = license.getValidUntil(); + if (validUntil != null) + { + Integer days = license.getDays(); + Integer remainingDays = license.getRemainingDays(); + + msg += " limited to " + days + " days expiring " + validUntil + " (" + remainingDays + " days remaining)"; + } + else + { + msg += " (does not expire)"; + } + + + logger.info(msg); + } + + // Log Repository Descriptors + if (logger.isInfoEnabled()) + { + Descriptor serverDescriptor = descriptorService.getServerDescriptor(); + Descriptor installedRepoDescriptor = descriptorService.getInstalledRepositoryDescriptor(); + String serverEdition = serverDescriptor.getEdition(); + String serverVersion = serverDescriptor.getVersion(); + int serverSchemaVersion = serverDescriptor.getSchema(); + String installedRepoVersion = installedRepoDescriptor.getVersion(); + int installedSchemaVersion = installedRepoDescriptor.getSchema(); + logger.info(String.format("Alfresco started (%s): Current version %s schema %d - Installed version %s schema %d", + serverEdition, serverVersion, serverSchemaVersion, installedRepoVersion, installedSchemaVersion)); + } + } + + + @Override + protected void onShutdown(ApplicationEvent event) + { + // NOOP + } } \ No newline at end of file diff --git a/source/java/org/alfresco/repo/dictionary/DictionaryDAOTest.java b/source/java/org/alfresco/repo/dictionary/DictionaryDAOTest.java index 1552ef870f..35190344ee 100644 --- a/source/java/org/alfresco/repo/dictionary/DictionaryDAOTest.java +++ b/source/java/org/alfresco/repo/dictionary/DictionaryDAOTest.java @@ -16,6 +16,7 @@ */ package org.alfresco.repo.dictionary; +import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -272,5 +273,26 @@ public class DictionaryDAOTest extends TestCase assertFalse(test5); } - + + public void testPropertyOverride() + { + TypeDefinition type1 = service.getType(QName.createQName(TEST_URL, "overridetype1")); + Map props1 = type1.getProperties(); + PropertyDefinition prop1 = props1.get(QName.createQName(TEST_URL, "propoverride")); + String def1 = prop1.getDefaultValue(); + assertEquals("one", def1); + + TypeDefinition type2 = service.getType(QName.createQName(TEST_URL, "overridetype2")); + Map props2 = type2.getProperties(); + PropertyDefinition prop2 = props2.get(QName.createQName(TEST_URL, "propoverride")); + String def2 = prop2.getDefaultValue(); + assertEquals("two", def2); + + TypeDefinition type3 = service.getType(QName.createQName(TEST_URL, "overridetype3")); + Map props3 = type3.getProperties(); + PropertyDefinition prop3 = props3.get(QName.createQName(TEST_URL, "propoverride")); + String def3 = prop3.getDefaultValue(); + assertEquals("three", def3); + } + } diff --git a/source/java/org/alfresco/repo/dictionary/TestModel.java b/source/java/org/alfresco/repo/dictionary/TestModel.java index d2bcb4be54..240be4ce7b 100644 --- a/source/java/org/alfresco/repo/dictionary/TestModel.java +++ b/source/java/org/alfresco/repo/dictionary/TestModel.java @@ -43,6 +43,8 @@ public class TestModel bootstrapModels.add("alfresco/model/systemModel.xml"); bootstrapModels.add("alfresco/model/contentModel.xml"); bootstrapModels.add("alfresco/model/applicationModel.xml"); + bootstrapModels.add("alfresco/model/bpmModel.xml"); + bootstrapModels.add("alfresco/workflow/workflowModel.xml"); // include models specified on command line for (String arg: args) diff --git a/source/java/org/alfresco/repo/dictionary/dictionarydaotest_model.xml b/source/java/org/alfresco/repo/dictionary/dictionarydaotest_model.xml index ba51937ef7..500c04df53 100644 --- a/source/java/org/alfresco/repo/dictionary/dictionarydaotest_model.xml +++ b/source/java/org/alfresco/repo/dictionary/dictionarydaotest_model.xml @@ -188,6 +188,33 @@ + + + + d:text + one + + + + + + test:overridetype1 + + + two + + + + + + test:overridetype2 + + + three + + + + diff --git a/source/java/org/alfresco/repo/domain/hibernate/Permission.hbm.xml b/source/java/org/alfresco/repo/domain/hibernate/Permission.hbm.xml index c79ac4586d..87d9900356 100644 --- a/source/java/org/alfresco/repo/domain/hibernate/Permission.hbm.xml +++ b/source/java/org/alfresco/repo/domain/hibernate/Permission.hbm.xml @@ -144,4 +144,14 @@ ace.authority.recipient = :authorityRecipient + + select + entry + from + org.alfresco.repo.domain.hibernate.DbAccessControlEntryImpl entry + where + entry.permission.typeQname = :oldTypeQName and + entry.permission.name = :oldName + + \ No newline at end of file diff --git a/source/java/org/alfresco/repo/domain/hibernate/Transaction.hbm.xml b/source/java/org/alfresco/repo/domain/hibernate/Transaction.hbm.xml index 61d3314746..46e95aef57 100644 --- a/source/java/org/alfresco/repo/domain/hibernate/Transaction.hbm.xml +++ b/source/java/org/alfresco/repo/domain/hibernate/Transaction.hbm.xml @@ -70,6 +70,14 @@ status.key.identifier = :identifier + + select + max(txn.id) + from + org.alfresco.repo.domain.hibernate.NodeStatusImpl as status + join status.transaction as txn + + select count(txn.id) @@ -90,6 +98,21 @@ ]]> + + :lastTxnId and + server.ipAddress != :serverIpAddress + order by + txn.id + ]]> + + select count(status.key.guid) @@ -98,9 +121,7 @@ join status.transaction as txn where txn.id = :txnId and - status.node is not null and - status.key.protocol = :protocol and - status.key.identifier = :identifier + status.node is not null @@ -111,9 +132,7 @@ join status.transaction as txn where txn.id = :txnId and - status.node is null and - status.key.protocol = :protocol and - status.key.identifier = :identifier + status.node is null diff --git a/source/java/org/alfresco/repo/domain/schema/SchemaBootstrap.java b/source/java/org/alfresco/repo/domain/schema/SchemaBootstrap.java index 64d718da6f..9d977ad485 100644 --- a/source/java/org/alfresco/repo/domain/schema/SchemaBootstrap.java +++ b/source/java/org/alfresco/repo/domain/schema/SchemaBootstrap.java @@ -25,7 +25,9 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.Writer; import java.sql.Connection; +import java.sql.DatabaseMetaData; import java.sql.ResultSet; +import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.List; @@ -33,6 +35,9 @@ import java.util.List; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.i18n.I18NUtil; import org.alfresco.repo.admin.patch.impl.SchemaUpgradeScriptPatch; +import org.alfresco.repo.content.filestore.FileContentWriter; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.util.AbstractLifecycleBean; import org.alfresco.util.TempFileProvider; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -45,8 +50,6 @@ import org.hibernate.tool.hbm2ddl.DatabaseMetadata; import org.hibernate.tool.hbm2ddl.SchemaExport; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationEvent; -import org.springframework.context.ApplicationListener; -import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.core.io.support.ResourcePatternResolver; @@ -58,12 +61,13 @@ import org.springframework.orm.hibernate3.LocalSessionFactoryBean; * * @author Derek Hulley */ -public class SchemaBootstrap implements ApplicationListener +public class SchemaBootstrap extends AbstractLifecycleBean { /** The placeholder for the configured Dialect class name: ${db.script.dialect} */ private static final String PLACEHOLDER_SCRIPT_DIALECT = "\\$\\{db\\.script\\.dialect\\}"; private static final String MSG_EXECUTING_SCRIPT = "schema.update.msg.executing_script"; + private static final String ERR_STATEMENT_FAILED = "schema.update.err.statement_failed"; private static final String ERR_UPDATE_FAILED = "schema.update.err.update_failed"; private static final String ERR_VALIDATION_FAILED = "schema.update.err.validation_failed"; private static final String ERR_SCRIPT_NOT_RUN = "schema.update.err.update_script_not_run"; @@ -148,58 +152,6 @@ public class SchemaBootstrap implements ApplicationListener this.applyUpdateScriptPatches = scriptPatches; } - public void onApplicationEvent(ApplicationEvent event) - { - if (!(event instanceof ContextRefreshedEvent)) - { - // only work on startup - return; - } - - // do everything in a transaction - Session session = getLocalSessionFactory().openSession(); - Transaction transaction = session.beginTransaction(); - try - { - // make sure that we don't autocommit - Connection connection = session.connection(); - connection.setAutoCommit(false); - - Configuration cfg = localSessionFactory.getConfiguration(); - // dump the schema, if required - if (schemaOuputFilename != null) - { - File schemaOutputFile = new File(schemaOuputFilename); - dumpSchemaCreate(cfg, schemaOutputFile); - } - - // update the schema, if required - if (updateSchema) - { - updateSchema(cfg, session, connection); - } - - // verify that all patches have been applied correctly - checkSchemaPatchScripts(cfg, session, connection, validateUpdateScriptPatches, false); // check scripts - checkSchemaPatchScripts(cfg, session, connection, applyUpdateScriptPatches, false); // check scripts - - // all done successfully - transaction.commit(); - } - catch (Throwable e) - { - try { transaction.rollback(); } catch (Throwable ee) {} - if (updateSchema) - { - throw new AlfrescoRuntimeException(ERR_UPDATE_FAILED, e); - } - else - { - throw new AlfrescoRuntimeException(ERR_VALIDATION_FAILED, e); - } - } - } - private void dumpSchemaCreate(Configuration cfg, File schemaOutputFile) { // if the file exists, delete it @@ -220,39 +172,78 @@ public class SchemaBootstrap implements ApplicationListener return (SessionFactory) localSessionFactory.getObject(); } + private static class NoSchemaException extends Exception + { + private static final long serialVersionUID = 5574280159910824660L; + } + /** * @return Returns the number of applied patches */ private int countAppliedPatches(Connection connection) throws Exception { - Statement stmt = connection.createStatement(); + DatabaseMetaData dbMetadata = connection.getMetaData(); + + ResultSet tableRs = dbMetadata.getTables(null, null, "%", null); + boolean newPatchTable = false; + boolean oldPatchTable = false; try { - ResultSet rs = stmt.executeQuery("select count(id) from alf_applied_patch"); - rs.next(); - int count = rs.getInt(1); - return count; - } - catch (Throwable e) - { - // we'll try another table name + while (tableRs.next()) + { + String tableName = tableRs.getString("TABLE_NAME"); + if (tableName.equalsIgnoreCase("applied_patch")) + { + oldPatchTable = true; + break; + } + else if (tableName.equalsIgnoreCase("alf_applied_patch")) + { + newPatchTable = true; + break; + } + } } finally { - try { stmt.close(); } catch (Throwable e) {} + try { tableRs.close(); } catch (Throwable e) {e.printStackTrace(); } } - // for pre-1.4 databases, the table was named differently - stmt = connection.createStatement(); - try + + if (newPatchTable) { - ResultSet rs = stmt.executeQuery("select count(id) from applied_patch"); - rs.next(); - int count = rs.getInt(1); - return count; + Statement stmt = connection.createStatement(); + try + { + ResultSet rs = stmt.executeQuery("select count(id) from alf_applied_patch"); + rs.next(); + int count = rs.getInt(1); + return count; + } + finally + { + try { stmt.close(); } catch (Throwable e) {} + } } - finally + else if (oldPatchTable) { - try { stmt.close(); } catch (Throwable e) {} + // found the old style table name + Statement stmt = connection.createStatement(); + try + { + ResultSet rs = stmt.executeQuery("select count(id) from applied_patch"); + rs.next(); + int count = rs.getInt(1); + return count; + } + finally + { + try { stmt.close(); } catch (Throwable e) {} + } + } + else + { + // The applied patches table is not present + throw new NoSchemaException(); } } @@ -308,22 +299,21 @@ public class SchemaBootstrap implements ApplicationListener { countAppliedPatches(connection); } - catch (Throwable e) + catch (NoSchemaException e) { create = true; } + // Get the dialect + final Dialect dialect = Dialect.getDialect(cfg.getProperties()); + String dialectStr = dialect.getClass().getName(); + if (create) { - // Get the dialect - final Dialect dialect = Dialect.getDialect(cfg.getProperties()); - String dialectStr = dialect.getClass().getName(); - // the applied patch table is missing - we assume that all other tables are missing // perform a full update using Hibernate-generated statements File tempFile = TempFileProvider.createTempFile("AlfrescoSchemaCreate-" + dialectStr + "-", ".sql"); dumpSchemaCreate(cfg, tempFile); - FileInputStream tempInputStream = new FileInputStream(tempFile); - executeScriptFile(cfg, connection, tempInputStream, tempFile.getPath()); + executeScriptFile(cfg, connection, tempFile, tempFile.getPath()); // execute post-create scripts (not patches) for (String scriptUrl : this.postCreateScriptUrls) { @@ -340,12 +330,11 @@ public class SchemaBootstrap implements ApplicationListener Writer writer = null; try { - final Dialect dialect = Dialect.getDialect(cfg.getProperties()); DatabaseMetadata metadata = new DatabaseMetadata(connection, dialect); String[] sqls = cfg.generateSchemaUpdateScript(dialect, metadata); if (sqls.length > 0) { - tempFile = TempFileProvider.createTempFile("AlfrescoSchemaUpdate", ".sql"); + tempFile = TempFileProvider.createTempFile("AlfrescoSchemaUpdate-" + dialectStr + "-", ".sql"); writer = new BufferedWriter(new FileWriter(tempFile)); for (String sql : sqls) { @@ -364,8 +353,7 @@ public class SchemaBootstrap implements ApplicationListener // execute if there were changes raised by Hibernate if (tempFile != null) { - InputStream tempInputStream = new FileInputStream(tempFile); - executeScriptFile(cfg, connection, tempInputStream, tempFile.getPath()); + executeScriptFile(cfg, connection, tempFile, tempFile.getPath()); } } } @@ -414,14 +402,27 @@ public class SchemaBootstrap implements ApplicationListener private void executeScriptUrl(Configuration cfg, Connection connection, String scriptUrl) throws Exception { Dialect dialect = Dialect.getDialect(cfg.getProperties()); + String dialectStr = dialect.getClass().getName(); InputStream scriptInputStream = getScriptInputStream(dialect.getClass(), scriptUrl); // check that it exists if (scriptInputStream == null) { throw AlfrescoRuntimeException.create(ERR_SCRIPT_NOT_FOUND, scriptUrl); } + // write the script to a temp location for future and failure reference + File tempFile = null; + try + { + tempFile = TempFileProvider.createTempFile("AlfrescoSchemaUpdate-" + dialectStr + "-", ".sql"); + ContentWriter writer = new FileContentWriter(tempFile); + writer.putContent(scriptInputStream); + } + finally + { + try { scriptInputStream.close(); } catch (Throwable e) {} // usually a duplicate close + } // now execute it - executeScriptFile(cfg, connection, scriptInputStream, scriptUrl); + executeScriptFile(cfg, connection, tempFile, scriptUrl); } /** @@ -463,11 +464,12 @@ public class SchemaBootstrap implements ApplicationListener private void executeScriptFile( Configuration cfg, Connection connection, - InputStream scriptInputStream, + File scriptFile, String scriptUrl) throws Exception { logger.info(I18NUtil.getMessage(MSG_EXECUTING_SCRIPT, scriptUrl)); + InputStream scriptInputStream = new FileInputStream(scriptFile); BufferedReader reader = new BufferedReader(new InputStreamReader(scriptInputStream, "UTF8")); try { @@ -512,21 +514,9 @@ public class SchemaBootstrap implements ApplicationListener // execute, if required if (execute) { - Statement stmt = connection.createStatement(); - try - { - sql = sb.toString(); - if (logger.isDebugEnabled()) - { - logger.debug("Executing statment: " + sql); - } - stmt.execute(sql); - sb = new StringBuilder(1024); - } - finally - { - try { stmt.close(); } catch (Throwable e) {} - } + sql = sb.toString(); + executeStatement(connection, sql, line, scriptFile); + sb = new StringBuilder(1024); } } } @@ -536,4 +526,85 @@ public class SchemaBootstrap implements ApplicationListener try { scriptInputStream.close(); } catch (Throwable e) {} } } + + /** + * Execute the given SQL statement, absorbing exceptions that we expect during + * schema creation or upgrade. + */ + private void executeStatement(Connection connection, String sql, int line, File file) throws Exception + { + Statement stmt = connection.createStatement(); + try + { + if (logger.isDebugEnabled()) + { + logger.debug("Executing statment: " + sql); + } + stmt.execute(sql); + } + catch (SQLException e) + { + String msg = I18NUtil.getMessage(ERR_STATEMENT_FAILED, sql, e.getMessage(), file.getAbsolutePath(), line); + // ignore exceptions generated by the creation of indexes that already exist + logger.error(msg); + throw e; + } + finally + { + try { stmt.close(); } catch (Throwable e) {} + } + } + + @Override + protected void onBootstrap(ApplicationEvent event) + { + // do everything in a transaction + Session session = getLocalSessionFactory().openSession(); + Transaction transaction = session.beginTransaction(); + try + { + // make sure that we don't autocommit + Connection connection = session.connection(); + connection.setAutoCommit(false); + + Configuration cfg = localSessionFactory.getConfiguration(); + // dump the schema, if required + if (schemaOuputFilename != null) + { + File schemaOutputFile = new File(schemaOuputFilename); + dumpSchemaCreate(cfg, schemaOutputFile); + } + + // update the schema, if required + if (updateSchema) + { + updateSchema(cfg, session, connection); + } + + // verify that all patches have been applied correctly + checkSchemaPatchScripts(cfg, session, connection, validateUpdateScriptPatches, false); // check scripts + checkSchemaPatchScripts(cfg, session, connection, applyUpdateScriptPatches, false); // check scripts + + // all done successfully + transaction.commit(); + } + catch (Throwable e) + { + try { transaction.rollback(); } catch (Throwable ee) {} + if (updateSchema) + { + throw new AlfrescoRuntimeException(ERR_UPDATE_FAILED, e); + } + else + { + throw new AlfrescoRuntimeException(ERR_VALIDATION_FAILED, e); + } + } + } + + @Override + protected void onShutdown(ApplicationEvent event) + { + // NOOP + } } diff --git a/source/java/org/alfresco/repo/importer/ImporterBootstrap.java b/source/java/org/alfresco/repo/importer/ImporterBootstrap.java index 12556807e0..6eb6ca27fb 100644 --- a/source/java/org/alfresco/repo/importer/ImporterBootstrap.java +++ b/source/java/org/alfresco/repo/importer/ImporterBootstrap.java @@ -46,6 +46,7 @@ import org.alfresco.service.cmr.view.Location; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.AbstractLifecycleBean; import org.alfresco.util.TempFileProvider; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -53,8 +54,6 @@ import org.apache.commons.logging.impl.Log4JLogger; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.springframework.context.ApplicationEvent; -import org.springframework.context.ApplicationListener; -import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.util.FileCopyUtils; /** @@ -62,7 +61,7 @@ import org.springframework.util.FileCopyUtils; * * @author David Caruana */ -public class ImporterBootstrap implements ApplicationListener +public class ImporterBootstrap extends AbstractLifecycleBean { // View Properties (used in setBootstrapViews) public static final String VIEW_PATH_PROPERTY = "path"; @@ -643,16 +642,16 @@ public class ImporterBootstrap implements ApplicationListener return true; } - /* - * (non-Javadoc) - * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent) - */ - public void onApplicationEvent(ApplicationEvent event) + @Override + protected void onBootstrap(ApplicationEvent event) { - if (event instanceof ContextRefreshedEvent) - { - bootstrap(); - } + bootstrap(); + } + + @Override + protected void onShutdown(ApplicationEvent event) + { + // NOOP } } diff --git a/source/java/org/alfresco/repo/importer/system/SystemExporterImporter.java b/source/java/org/alfresco/repo/importer/system/SystemExporterImporter.java index 20a3271d59..adbc5320ba 100644 --- a/source/java/org/alfresco/repo/importer/system/SystemExporterImporter.java +++ b/source/java/org/alfresco/repo/importer/system/SystemExporterImporter.java @@ -22,7 +22,7 @@ import java.util.List; import org.alfresco.repo.admin.patch.PatchDaoService; import org.alfresco.repo.domain.AppliedPatch; -import org.alfresco.repo.domain.hibernate.VersionCounterDaoComponentImpl; +import org.alfresco.repo.version.common.counter.VersionCounterService; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.StoreRef; @@ -37,7 +37,7 @@ public class SystemExporterImporter // dependencies private NodeService nodeService; private PatchDaoService patchDao; - private VersionCounterDaoComponentImpl versionCounterDao; + private VersionCounterService versionCounterService; public void setNodeService(NodeService nodeService) @@ -50,9 +50,9 @@ public class SystemExporterImporter this.patchDao = patchDaoService; } - public void setVersionDao(VersionCounterDaoComponentImpl versionCounterDao) + public void setVersionCounterService(VersionCounterService versionCounterService) { - this.versionCounterDao = versionCounterDao; + this.versionCounterService = versionCounterService; } @@ -89,7 +89,7 @@ public class SystemExporterImporter for (StoreRef storeRef : storeRefs) { VersionCounterInfo versionCounterInfo = new VersionCounterInfo(); - int versionCount = versionCounterDao.currentVersionNumber(storeRef); + int versionCount = versionCounterService.currentVersionNumber(storeRef); versionCounterInfo.storeRef = storeRef.toString(); versionCounterInfo.count = versionCount; systemInfo.versionCounters.add(versionCounterInfo); @@ -128,7 +128,7 @@ public class SystemExporterImporter for (VersionCounterInfo versionCounterInfo : systemInfo.versionCounters) { StoreRef storeRef = new StoreRef(versionCounterInfo.storeRef); - versionCounterDao.setVersionNumber(storeRef, versionCounterInfo.count); + versionCounterService.setVersionNumber(storeRef, versionCounterInfo.count); } } diff --git a/source/java/org/alfresco/repo/importer/system/SystemInfoBootstrap.java b/source/java/org/alfresco/repo/importer/system/SystemInfoBootstrap.java index e764c2e751..5983a32379 100644 --- a/source/java/org/alfresco/repo/importer/system/SystemInfoBootstrap.java +++ b/source/java/org/alfresco/repo/importer/system/SystemInfoBootstrap.java @@ -26,9 +26,8 @@ import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.service.cmr.view.ImporterException; import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.AbstractLifecycleBean; import org.springframework.context.ApplicationEvent; -import org.springframework.context.ApplicationListener; -import org.springframework.context.event.ContextRefreshedEvent; /** @@ -36,7 +35,7 @@ import org.springframework.context.event.ContextRefreshedEvent; * * @author davidc */ -public class SystemInfoBootstrap implements ApplicationListener +public class SystemInfoBootstrap extends AbstractLifecycleBean { // dependencies private TransactionService transactionService; @@ -177,16 +176,16 @@ public class SystemInfoBootstrap implements ApplicationListener return true; } - /* - * (non-Javadoc) - * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent) - */ - public void onApplicationEvent(ApplicationEvent event) + @Override + protected void onBootstrap(ApplicationEvent event) { - if (event instanceof ContextRefreshedEvent) - { - bootstrap(); - } + bootstrap(); + } + + @Override + protected void onShutdown(ApplicationEvent event) + { + // NOOP } } diff --git a/source/java/org/alfresco/repo/jscript/Actions.java b/source/java/org/alfresco/repo/jscript/Actions.java index b371b1b5aa..9021fe1abf 100644 --- a/source/java/org/alfresco/repo/jscript/Actions.java +++ b/source/java/org/alfresco/repo/jscript/Actions.java @@ -28,32 +28,32 @@ import org.alfresco.service.cmr.action.ParameterDefinition; import org.alfresco.service.cmr.dictionary.DataTypeDefinition; import org.alfresco.service.namespace.QName; import org.mozilla.javascript.Scriptable; - +import org.mozilla.javascript.Wrapper; /** * Scripted Action service for describing and executing actions against Nodes. - * + * * @author davidc */ public final class Actions implements Scopeable { /** Repository Service Registry */ private ServiceRegistry services; - + /** Root scope for this object */ private Scriptable scope; - /** * Constructor * - * @param services repository service registry + * @param services + * repository service registry */ public Actions(ServiceRegistry services) { this.services = services; } - + /** * @see org.alfresco.repo.jscript.Scopeable#setScope(org.mozilla.javascript.Scriptable) */ @@ -61,11 +61,11 @@ public final class Actions implements Scopeable { this.scope = scope; } - + /** * Gets the list of registered action names * - * @return the registered action names + * @return the registered action names */ public String[] getRegistered() { @@ -79,17 +79,18 @@ public final class Actions implements Scopeable } return registered; } - + public String[] jsGet_registered() { return getRegistered(); } - + /** * Create an Action * - * @param actionName the action name - * @return the action + * @param actionName + * the action name + * @return the action */ public ScriptAction create(String actionName) { @@ -104,8 +105,7 @@ public final class Actions implements Scopeable } return scriptAction; } - - + /** * Scriptable Action * @@ -114,23 +114,25 @@ public final class Actions implements Scopeable public final class ScriptAction implements Serializable, Scopeable { private static final long serialVersionUID = 5794161358406531996L; - + /** Root scope for this object */ - private Scriptable scope; + private Scriptable scope; /** Converter with knowledge of action parameter values */ private ActionValueConverter converter; - + /** Action state */ private Action action; + private ActionDefinition actionDef; + private ScriptableParameterMap parameters = null; - - + /** * Construct * - * @param action Alfresco action + * @param action + * Alfresco action */ public ScriptAction(Action action, ActionDefinition actionDef) { @@ -138,7 +140,7 @@ public final class Actions implements Scopeable this.actionDef = actionDef; this.converter = new ActionValueConverter(); } - + /** * @see org.alfresco.repo.jscript.Scopeable#setScope(org.mozilla.javascript.Scriptable) */ @@ -146,28 +148,25 @@ public final class Actions implements Scopeable { this.scope = scope; } - + /** * Returns the action name * - * @return action name + * @return action name */ public String getName() { return this.actionDef.getName(); } - + public String jsGet_name() { return getName(); } - + /** - * Return all the properties known about this node. - * - * The Map returned implements the Scriptable interface to allow access to the properties via - * JavaScript associative array access. This means properties of a node can be access thus: - * node.properties["name"] + * Return all the properties known about this node. The Map returned implements the Scriptable interface to allow access to the properties via JavaScript associative array + * access. This means properties of a node can be access thus: node.properties["name"] * * @return Map of properties for this Node. */ @@ -187,17 +186,18 @@ public final class Actions implements Scopeable this.parameters.setModified(false); } return this.parameters; - } - + } + public Map jsGet_parameters() { return getParameters(); } - + /** * Execute action * - * @param node the node to execute action upon + * @param node + * the node to execute action upon */ @SuppressWarnings("synthetic-access") public void execute(Node node) @@ -206,7 +206,7 @@ public final class Actions implements Scopeable { Map actionParams = action.getParameterValues(); actionParams.clear(); - + for (Map.Entry entry : this.parameters.entrySet()) { // perform the conversion from script wrapper object to repo serializable values @@ -217,7 +217,7 @@ public final class Actions implements Scopeable } services.getActionService().executeAction(action, node.getNodeRef()); } - + /** * Value converter with specific knowledge of action parameters * @@ -227,10 +227,12 @@ public final class Actions implements Scopeable { /** * Convert Action Parameter for Script usage - * - * @param paramName parameter name - * @param value value to convert - * @return converted value + * + * @param paramName + * parameter name + * @param value + * value to convert + * @return converted value */ @SuppressWarnings("synthetic-access") public Serializable convertActionParamForScript(String paramName, Serializable value) @@ -238,7 +240,7 @@ public final class Actions implements Scopeable ParameterDefinition paramDef = actionDef.getParameterDefintion(paramName); if (paramDef != null && paramDef.getType().equals(DataTypeDefinition.QNAME)) { - return ((QName)value).toPrefixString(services.getNamespaceService()); + return ((QName) value).toPrefixString(services.getNamespaceService()); } else { @@ -249,17 +251,45 @@ public final class Actions implements Scopeable /** * Convert Action Parameter for Java usage * - * @param paramName parameter name - * @param value value to convert - * @return converted value + * @param paramName + * parameter name + * @param value + * value to convert + * @return converted value */ @SuppressWarnings("synthetic-access") public Serializable convertActionParamForRepo(String paramName, Serializable value) { ParameterDefinition paramDef = actionDef.getParameterDefintion(paramName); + if (paramDef != null && paramDef.getType().equals(DataTypeDefinition.QNAME)) { - return QName.createQName((String)value, services.getNamespaceService()); + if (value instanceof Wrapper) + { + // unwrap a Java object from a JavaScript wrapper + // recursively call this method to convert the unwrapped value + return convertActionParamForRepo(paramName, (Serializable) ((Wrapper) value).unwrap()); + } + else + { + if (value instanceof String) + { + String stringQName = (String) value; + if (stringQName.startsWith("{")) + { + return QName.createQName(stringQName); + + } + else + { + return QName.createQName(stringQName, services.getNamespaceService()); + } + } + else + { + return value; + } + } } else { @@ -269,39 +299,41 @@ public final class Actions implements Scopeable } } - /** * Scripted Parameter map with modified flag. - * + * * @author davidc */ - public static final class ScriptableParameterMap extends ScriptableHashMap + public static final class ScriptableParameterMap extends ScriptableHashMap { private static final long serialVersionUID = 574661815973241554L; - private boolean modified = false; + private boolean modified = false; /** * Is this a modified parameter map? * - * @return true => modified + * @return true => modified */ - /*package*/ boolean isModified() + /* package */boolean isModified() { return modified; } - + /** * Set explicitly whether this map is modified * - * @param modified true => modified, false => not modified + * @param modified + * true => modified, false => not modified */ - /*package*/ void setModified(boolean modified) + /* package */void setModified(boolean modified) { this.modified = modified; } - - /* (non-Javadoc) + + /* + * (non-Javadoc) + * * @see org.mozilla.javascript.Scriptable#getClassName() */ @Override @@ -310,7 +342,9 @@ public final class Actions implements Scopeable return "ScriptableParameterMap"; } - /* (non-Javadoc) + /* + * (non-Javadoc) + * * @see org.mozilla.javascript.Scriptable#delete(java.lang.String) */ @Override @@ -320,7 +354,9 @@ public final class Actions implements Scopeable setModified(true); } - /* (non-Javadoc) + /* + * (non-Javadoc) + * * @see org.mozilla.javascript.Scriptable#put(java.lang.String, org.mozilla.javascript.Scriptable, java.lang.Object) */ @Override @@ -330,5 +366,5 @@ public final class Actions implements Scopeable setModified(true); } } - + } diff --git a/source/java/org/alfresco/repo/jscript/CategoryNode.java b/source/java/org/alfresco/repo/jscript/CategoryNode.java new file mode 100644 index 0000000000..fc44e54e0e --- /dev/null +++ b/source/java/org/alfresco/repo/jscript/CategoryNode.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.jscript; + +import java.util.Collection; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.TemplateImageResolver; +import org.alfresco.service.cmr.search.CategoryService; +import org.alfresco.service.namespace.QName; +import org.mozilla.javascript.Scriptable; + +/** + * Category Nodes from the classification helper have special support. + * + * @author Andy Hind + */ +public class CategoryNode extends Node +{ + /** + * Constructor + * + * @param nodeRef + * @param services + * @param resolver + */ + public CategoryNode(NodeRef nodeRef, ServiceRegistry services, TemplateImageResolver resolver) + { + super(nodeRef, services, resolver); + } + + /** + * Constructor + * + * @param nodeRef + * @param services + * @param resolver + * @param scope + */ + public CategoryNode(NodeRef nodeRef, ServiceRegistry services, TemplateImageResolver resolver, Scriptable scope) + { + super(nodeRef, services, resolver, scope); + } + + /** + * @return all the members of a category + */ + public Node[] getCategoryMembers() + { + return buildNodes(services.getCategoryService().getChildren(getNodeRef(), CategoryService.Mode.MEMBERS, CategoryService.Depth.ANY)); + } + + public Node[] jsGet_categoryMembers() + { + return getCategoryMembers(); + } + + /** + * @return all the subcategories of a category + */ + public CategoryNode[] getSubCategories() + { + return buildCategoryNodes(services.getCategoryService().getChildren(getNodeRef(), CategoryService.Mode.SUB_CATEGORIES, CategoryService.Depth.ANY)); + } + + public CategoryNode[] jsGet_subCategories() + { + return getSubCategories(); + } + + /** + * @return members and subcategories of a category + */ + public Node[] getMembersAndSubCategories() + { + return buildMixedNodes(services.getCategoryService().getChildren(getNodeRef(), CategoryService.Mode.ALL, CategoryService.Depth.ANY)); + } + + public Node[] jsGet_membersAndSubCategories() + { + return getMembersAndSubCategories(); + } + + /** + * @return all the immediate member of a category + */ + public Node[] getImmediateCategoryMembers() + { + return buildNodes(services.getCategoryService().getChildren(getNodeRef(), CategoryService.Mode.MEMBERS, CategoryService.Depth.IMMEDIATE)); + } + + public Node[] jsGet_immediateCategoryMembers() + { + return getImmediateCategoryMembers(); + } + + /** + * @return all the immediate subcategories of a category + */ + public CategoryNode[] getImmediateSubCategories() + { + return buildCategoryNodes(services.getCategoryService().getChildren(getNodeRef(), CategoryService.Mode.SUB_CATEGORIES, CategoryService.Depth.IMMEDIATE)); + } + + public CategoryNode[] jsGet_immediateSubCategories() + { + return getImmediateSubCategories(); + } + + /** + * @return immediate members and subcategories of a category + */ + public Node[] getImmediateMembersAndSubCategories() + { + return buildMixedNodes(services.getCategoryService().getChildren(getNodeRef(), CategoryService.Mode.ALL, CategoryService.Depth.IMMEDIATE)); + } + + public Node[] jsGet_immediateMembersAndSubCategories() + { + return getImmediateMembersAndSubCategories(); + } + + /** + * Create a new subcategory + * + * @param name Of the category to create + * + * @return CategoryNode + */ + public CategoryNode createSubCategory(String name) + { + return new CategoryNode(services.getCategoryService().createCategory(getNodeRef(), name), this.services, this.imageResolver, this.scope); + } + + /** + * Remove this category + */ + public void removeCategory() + { + services.getCategoryService().deleteCategory(getNodeRef()); + } + + @Override + public boolean isCategory() + { + return true; + } + + private CategoryNode[] buildCategoryNodes(Collection cars) + { + CategoryNode[] categoryNodes = new CategoryNode[cars.size()]; + int i = 0; + for (ChildAssociationRef car : cars) + { + categoryNodes[i++] = new CategoryNode(car.getChildRef(), this.services, this.imageResolver, this.scope); + } + return categoryNodes; + } + + private Node[] buildNodes(Collection cars) + { + Node[] nodes = new Node[cars.size()]; + int i = 0; + for (ChildAssociationRef car : cars) + { + nodes[i++] = new Node(car.getChildRef(), this.services, this.imageResolver, this.scope); + } + return nodes; + } + + private Node[] buildMixedNodes(Collection cars) + { + Node[] nodes = new Node[cars.size()]; + int i = 0; + for (ChildAssociationRef car : cars) + { + QName type = services.getNodeService().getType(car.getChildRef()); + if (services.getDictionaryService().isSubClass(type, ContentModel.TYPE_CATEGORY)) + { + nodes[i++] = new CategoryNode(car.getChildRef(), this.services, this.imageResolver, this.scope); + } + else + { + nodes[i++] = new Node(car.getChildRef(), this.services, this.imageResolver, this.scope); + } + } + return nodes; + } +} diff --git a/source/java/org/alfresco/repo/jscript/CategoryTemplateNode.java b/source/java/org/alfresco/repo/jscript/CategoryTemplateNode.java new file mode 100644 index 0000000000..2db629bcc2 --- /dev/null +++ b/source/java/org/alfresco/repo/jscript/CategoryTemplateNode.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.jscript; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.TemplateImageResolver; +import org.alfresco.service.cmr.repository.TemplateNode; +import org.alfresco.service.cmr.search.CategoryService; +import org.alfresco.service.namespace.QName; + +/** + * Category Nodes from the classification helper have special support. + */ +public class CategoryTemplateNode extends TemplateNode +{ + /** + * Constructor + * + * @param nodeRef + * @param services + * @param resolver + */ + public CategoryTemplateNode(NodeRef nodeRef, ServiceRegistry services, TemplateImageResolver resolver) + { + super(nodeRef, services, resolver); + } + + @Override + public boolean getIsCategory() + { + return true; + } + + /** + * @return all the member of a category + */ + public List getCategoryMembers() + { + if (getIsCategory()) + { + return buildTemplateNodeList(services.getCategoryService().getChildren(getNodeRef(), + CategoryService.Mode.MEMBERS, CategoryService.Depth.ANY)); + } + else + { + return Collections.emptyList(); + } + } + + /** + * @return all the subcategories of a category + */ + public List getSubCategories() + { + if (getIsCategory()) + { + return buildCategoryNodeList(services.getCategoryService().getChildren(getNodeRef(), + CategoryService.Mode.SUB_CATEGORIES, CategoryService.Depth.ANY)); + } + else + { + return Collections.emptyList(); + } + } + + /** + * @return members and subcategories of a category + */ + public List getMembersAndSubCategories() + { + if (getIsCategory()) + { + + return buildMixedNodeList(services.getCategoryService().getChildren(getNodeRef(), CategoryService.Mode.ALL, + CategoryService.Depth.ANY)); + } + else + { + return Collections.emptyList(); + } + } + + /** + * @return all the immediate member of a category + */ + public List getImmediateCategoryMembers() + { + if (getIsCategory()) + { + return buildTemplateNodeList(services.getCategoryService().getChildren(getNodeRef(), + CategoryService.Mode.MEMBERS, CategoryService.Depth.IMMEDIATE)); + } + else + { + return Collections.emptyList(); + } + } + + /** + * @return all the immediate subcategories of a category + */ + public List getImmediateSubCategories() + { + if (getIsCategory()) + { + return buildCategoryNodeList(services.getCategoryService().getChildren(getNodeRef(), + CategoryService.Mode.SUB_CATEGORIES, CategoryService.Depth.IMMEDIATE)); + } + else + { + return Collections.emptyList(); + } + } + + /** + * @return immediate members and subcategories of a category + */ + public List getImmediateMembersAndSubCategories() + { + if (getIsCategory()) + { + return buildMixedNodeList(services.getCategoryService().getChildren(getNodeRef(), + CategoryService.Mode.ALL, CategoryService.Depth.IMMEDIATE)); + } + else + { + return Collections.emptyList(); + } + } + + /** + * Support to build node lists from category service API calls. + * + * @param childRefs + * + * @return List of TemplateNode + */ + private List buildTemplateNodeList(Collection childRefs) + { + List answer = new ArrayList(childRefs.size()); + for (ChildAssociationRef ref : childRefs) + { + // create our Node representation from the NodeRef + TemplateNode child = new TemplateNode(ref.getChildRef(), this.services, this.imageResolver); + answer.add(child); + } + return answer; + } + + private List buildCategoryNodeList(Collection childRefs) + { + List answer = new ArrayList(childRefs.size()); + for (ChildAssociationRef ref : childRefs) + { + // create our Node representation from the NodeRef + CategoryTemplateNode child = new CategoryTemplateNode(ref.getChildRef(), this.services, this.imageResolver); + answer.add(child); + } + return answer; + } + + private List buildMixedNodeList(Collection cars) + { + List nodes = new ArrayList(cars.size()); + int i = 0; + for (ChildAssociationRef car : cars) + { + QName type = services.getNodeService().getType(car.getChildRef()); + if (services.getDictionaryService().isSubClass(type, ContentModel.TYPE_CATEGORY)) + { + nodes.add(new CategoryTemplateNode(car.getChildRef(), this.services, this.imageResolver)); + } + else + { + nodes.add(new TemplateNode(car.getChildRef(), this.services, this.imageResolver)); + } + } + return nodes; + } +} diff --git a/source/java/org/alfresco/repo/jscript/Classification.java b/source/java/org/alfresco/repo/jscript/Classification.java new file mode 100644 index 0000000000..c093bf630d --- /dev/null +++ b/source/java/org/alfresco/repo/jscript/Classification.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.jscript; + +import java.util.Collection; + +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.TemplateImageResolver; +import org.alfresco.service.cmr.search.CategoryService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.mozilla.javascript.Scriptable; + +/** + * Support class for finding categories, finding root nodes for categories and creating root categories. + * + * @author Andy Hind + */ +public final class Classification implements Scopeable +{ + @SuppressWarnings("unused") + private Scriptable scope; + + private ServiceRegistry services; + + @SuppressWarnings("unused") + private TemplateImageResolver imageResolver; + + private StoreRef storeRef; + + public Classification(ServiceRegistry services, StoreRef storeRef, TemplateImageResolver imageResolver) + { + this.services = services; + this.imageResolver = imageResolver; + this.storeRef = storeRef; + } + + /** + * @see org.alfresco.repo.jscript.Scopeable#setScope(org.mozilla.javascript.Scriptable) + */ + public void setScope(Scriptable scope) + { + this.scope = scope; + } + + /** + * Find all the category nodes in a given classification. + * + * @param aspect + * @return + */ + public CategoryNode[] getAllCategoryNodes(String aspect) + { + return buildCategoryNodes(services.getCategoryService().getCategories(storeRef, createQName(aspect), + CategoryService.Depth.ANY)); + } + + /** + * Get all the aspects that define a classification. + * + * @return + */ + public String[] getAllClassificationAspects() + { + Collection aspects = services.getCategoryService().getClassificationAspects(); + String[] answer = new String[aspects.size()]; + int i = 0; + for (QName qname : aspects) + { + answer[i++] = qname.toPrefixString(this.services.getNamespaceService()); + } + return answer; + } + + public String[] jsGet_allClassificationAspects() + { + return getAllClassificationAspects(); + } + + /** + * Create a root category in a classification. + * + * @param aspect + * @param name + */ + public void createRootCategory(String aspect, String name) + { + services.getCategoryService().createRootCategory(storeRef, createQName(aspect), name); + } + + /** + * Get the root categories in a classification. + * + * @param aspect + * @return + */ + public CategoryNode[] getRootCategories(String aspect) + { + return buildCategoryNodes(services.getCategoryService().getRootCategories(storeRef, createQName(aspect))); + } + + private CategoryNode[] buildCategoryNodes(Collection cars) + { + CategoryNode[] categoryNodes = new CategoryNode[cars.size()]; + int i = 0; + for (ChildAssociationRef car : cars) + { + categoryNodes[i++] = new CategoryNode(car.getChildRef(), this.services, this.imageResolver, this.scope); + } + return categoryNodes; + } + + private QName createQName(String s) + { + QName qname; + if (s.indexOf(QName.NAMESPACE_BEGIN) != -1) + { + qname = QName.createQName(s); + } + else + { + qname = QName.createQName(s, this.services.getNamespaceService()); + } + return qname; + } + +} diff --git a/source/java/org/alfresco/repo/jscript/Node.java b/source/java/org/alfresco/repo/jscript/Node.java index daeb82ed3f..71acd81311 100644 --- a/source/java/org/alfresco/repo/jscript/Node.java +++ b/source/java/org/alfresco/repo/jscript/Node.java @@ -92,7 +92,7 @@ public class Node implements Serializable, Scopeable private final static String FOLDER_BROWSE_URL = "/navigate/browse/{0}/{1}/{2}"; /** Root scope for this object */ - private Scriptable scope; + protected Scriptable scope; /** Node Value Converter */ private NodeValueConverter converter = null; @@ -110,18 +110,17 @@ public class Node implements Serializable, Scopeable private Node[] children = null; /** The properties of this node */ private ScriptableQNameMap properties = null; - private ServiceRegistry services = null; + protected ServiceRegistry services = null; private NodeService nodeService = null; private Boolean isDocument = null; private Boolean isContainer = null; private String displayPath = null; - private TemplateImageResolver imageResolver = null; + protected TemplateImageResolver imageResolver = null; private Node parent = null; private ChildAssociationRef primaryParentAssoc = null; // NOTE: see the reset() method when adding new cached members! - // ------------------------------------------------------------------------------ // Construction @@ -464,6 +463,20 @@ public class Node implements Serializable, Scopeable return isDocument(); } + /** + * @return true if the Node is a Category + */ + public boolean isCategory() + { + // this valid is overriden by the CategoryNode sub-class + return false; + } + + public boolean jsGet_isCategory() + { + return isCategory(); + } + /** * @return The list of aspects applied to this node */ @@ -890,7 +903,8 @@ public class Node implements Serializable, Scopeable this.services.getPermissionService().deletePermission(this.nodeRef, authority, permission); } - // ------------- + + // ------------------------------------------------------------------------------ // Ownership API /** @@ -1145,7 +1159,7 @@ public class Node implements Serializable, Scopeable { if (destination != null) { - NodeRef copyRef = this.services.getCopyService().copy( + NodeRef copyRef = this.services.getCopyService().copyAndRename( this.nodeRef, destination.getNodeRef(), ContentModel.ASSOC_CONTAINS, diff --git a/source/java/org/alfresco/repo/jscript/RhinoScriptService.java b/source/java/org/alfresco/repo/jscript/RhinoScriptService.java index 9e501435c3..e711e7eb4e 100644 --- a/source/java/org/alfresco/repo/jscript/RhinoScriptService.java +++ b/source/java/org/alfresco/repo/jscript/RhinoScriptService.java @@ -220,25 +220,24 @@ public class RhinoScriptService implements ScriptService // add useful util objects model.put("actions", new Actions(services)); model.put("logger", new ScriptLogger()); + model.put("utils", new ScriptUtils()); // insert supplied object model into root of the default scope + for (String key : model.keySet()) { - for (String key : model.keySet()) + // set the root scope on appropriate objects + // this is used to allow native JS object creation etc. + Object obj = model.get(key); + if (obj instanceof Scopeable) { - // set the root scope on appropriate objects - // this is used to allow native JS object creation etc. - Object obj = model.get(key); - if (obj instanceof Scopeable) - { - ((Scopeable)obj).setScope(scope); - } - - // convert/wrap each object to JavaScript compatible - Object jsObject = Context.javaToJS(obj, scope); - - // insert into the root scope ready for access by the script - ScriptableObject.putProperty(scope, key, jsObject); + ((Scopeable)obj).setScope(scope); } + + // convert/wrap each object to JavaScript compatible + Object jsObject = Context.javaToJS(obj, scope); + + // insert into the root scope ready for access by the script + ScriptableObject.putProperty(scope, key, jsObject); } // execute the script @@ -343,6 +342,10 @@ public class RhinoScriptService implements ScriptService model.put("search", new Search(services, companyHome.getStoreRef(), resolver)); + model.put("session", new Session(services, resolver)); + + model.put("classification", new Classification(services, companyHome.getStoreRef(), resolver)); + return model; } } diff --git a/source/java/org/alfresco/repo/jscript/ScriptUtils.java b/source/java/org/alfresco/repo/jscript/ScriptUtils.java new file mode 100644 index 0000000000..853b3fe593 --- /dev/null +++ b/source/java/org/alfresco/repo/jscript/ScriptUtils.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.jscript; + +import org.mozilla.javascript.Scriptable; + +/** + * Place for general and miscellenous utility functions not already found in generic JavaScript. + * + * @author Kevin Roast + */ +public final class ScriptUtils implements Scopeable +{ + /** Root scope for this object */ + private Scriptable scope; + + /** + * @see org.alfresco.repo.jscript.Scopeable#setScope(org.mozilla.javascript.Scriptable) + */ + public void setScope(Scriptable scope) + { + this.scope = scope; + } + + /** + * Function to pad a string with zero '0' characters to the required length + * + * @param s String to pad with leading zero '0' characters + * @param len Length to pad to + * + * @return padded string or the original if already at >=len characters + */ + public String pad(String s, int len) + { + String result = s; + for (int i=0; i<(len - s.length()); i++) + { + result = "0" + result; + } + return result; + } +} diff --git a/source/java/org/alfresco/repo/jscript/Session.java b/source/java/org/alfresco/repo/jscript/Session.java new file mode 100644 index 0000000000..de54dfab33 --- /dev/null +++ b/source/java/org/alfresco/repo/jscript/Session.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.jscript; + +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.TemplateImageResolver; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.mozilla.javascript.Scriptable; + +/** + * Support object for session level properties etc. + *

    + * Provides access to the user's authentication ticket. + * + * @author Andy Hind + */ +public class Session implements Scopeable +{ + + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(Session.class); + + @SuppressWarnings("unused") + private Scriptable scope; + + private ServiceRegistry services; + + @SuppressWarnings("unused") + private TemplateImageResolver imageResolver; + + public Session(ServiceRegistry services, TemplateImageResolver imageResolver) + { + this.services = services; + this.imageResolver = imageResolver; + } + + /** + * @see org.alfresco.repo.jscript.Scopeable#setScope(org.mozilla.javascript.Scriptable) + */ + public void setScope(Scriptable scope) + { + this.scope = scope; + } + + /** + * Get the user's authentication ticket. + * + * @return + */ + public String getTicket() + { + return services.getAuthenticationService().getCurrentTicket(); + } + + /** + * Expose the user's authentication ticket as JavaScipt property. + * + * @return + */ + public String jsGet_ticket() + { + return getTicket(); + } +} diff --git a/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java index 6d5eff338d..950571e8c6 100644 --- a/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java +++ b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java @@ -503,7 +503,7 @@ public class FileFolderServiceImpl implements FileFolderService */ public FileInfo rename(NodeRef sourceNodeRef, String newName) throws FileExistsException, FileNotFoundException { - return move(sourceNodeRef, null, newName); + return moveOrCopy(sourceNodeRef, null, newName, true); } /** @@ -815,12 +815,25 @@ public class FileFolderServiceImpl implements FileFolderService for (int i = 0; i < folderCount; i++) { String pathElement = pathElements.get(i); - FileInfo pathElementInfo = getPathElementInfo(currentPath, rootNodeRef, parentNodeRef, pathElement, true); - parentNodeRef = pathElementInfo.getNodeRef(); + NodeRef folderNodeRef = searchSimple(parentNodeRef, pathElement); + if (folderNodeRef == null) + { + StringBuilder sb = new StringBuilder(128); + sb.append("Folder not found: " + currentPath); + throw new FileNotFoundException(sb.toString()); + } + parentNodeRef = folderNodeRef; } // we have resolved the folder path - resolve the last component String pathElement = pathElements.get(pathElements.size() - 1); - FileInfo result = getPathElementInfo(currentPath, rootNodeRef, parentNodeRef, pathElement, false); + NodeRef fileNodeRef = searchSimple(parentNodeRef, pathElement); + if (fileNodeRef == null) + { + StringBuilder sb = new StringBuilder(128); + sb.append("File not found: " + currentPath); + throw new FileNotFoundException(sb.toString()); + } + FileInfo result = getFileInfo(fileNodeRef); // found it if (logger.isDebugEnabled()) { @@ -831,42 +844,6 @@ public class FileFolderServiceImpl implements FileFolderService } return result; } - - /** - * Helper method to dig down a level for a node based on name - */ - private FileInfo getPathElementInfo( - StringBuilder currentPath, - NodeRef rootNodeRef, - NodeRef parentNodeRef, - String pathElement, - boolean folderOnly) throws FileNotFoundException - { - currentPath.append("/").append(pathElement); - - boolean includeFiles = (folderOnly ? false : true); - List pathElementInfos = search(parentNodeRef, pathElement, includeFiles, true, false); - // check - if (pathElementInfos.size() == 0) - { - StringBuilder sb = new StringBuilder(128); - sb.append(folderOnly ? "Folder" : "File or folder").append(" not found: \n") - .append(" root: ").append(rootNodeRef).append("\n") - .append(" path: ").append(currentPath); - throw new FileNotFoundException(sb.toString()); - } - else if (pathElementInfos.size() > 1) - { - // we have detected a duplicate name - warn, but allow - StringBuilder sb = new StringBuilder(128); - sb.append("Duplicate file or folder found: \n") - .append(" root: ").append(rootNodeRef).append("\n") - .append(" path: ").append(currentPath); - logger.warn(sb); - } - FileInfo pathElementInfo = pathElementInfos.get(0); - return pathElementInfo; - } public FileInfo getFileInfo(NodeRef nodeRef) { diff --git a/source/java/org/alfresco/repo/node/db/NodeDaoService.java b/source/java/org/alfresco/repo/node/db/NodeDaoService.java index c44fb7fca7..2e102f7aa0 100644 --- a/source/java/org/alfresco/repo/node/db/NodeDaoService.java +++ b/source/java/org/alfresco/repo/node/db/NodeDaoService.java @@ -236,11 +236,14 @@ public interface NodeDaoService */ public List getPropertyValuesByActualType(DataTypeDefinition actualDataTypeDefinition); - public Transaction getLastTxn(final StoreRef storeRef); - public int getTxnUpdateCountForStore(final StoreRef storeRef, final long txnId); - public int getTxnDeleteCountForStore(final StoreRef storeRef, final long txnId); + public Transaction getTxnById(long txnId); + public Transaction getLastTxn(); + public Transaction getLastTxnForStore(final StoreRef storeRef); + public int getTxnUpdateCount(final long txnId); + public int getTxnDeleteCount(final long txnId); public int getTransactionCount(); - public List getNextTxns(final Transaction lastTxn, final int count); + public List getNextTxns(final long lastTxnId, final int count); + public List getNextRemoteTxns(final long lastTxnId, final int count); public List getTxnChangesForStore(final StoreRef storeRef, final long txnId); public List getTxnChanges(final long txnId); } diff --git a/source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java b/source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java index e849b82834..cc702dc894 100644 --- a/source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java +++ b/source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java @@ -18,6 +18,7 @@ package org.alfresco.repo.node.db.hibernate; import java.io.Serializable; import java.net.InetAddress; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -97,6 +98,7 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements private final String uuid; private static TransactionAwareSingleton serverIdSingleton = new TransactionAwareSingleton(); + private final String ipAddress; /** * @@ -104,6 +106,14 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements public HibernateNodeDaoServiceImpl() { this.uuid = GUID.generate(); + try + { + ipAddress = InetAddress.getLocalHost().getHostAddress(); + } + catch (UnknownHostException e) + { + throw new AlfrescoRuntimeException("Failed to get server IP address", e); + } } /** @@ -148,7 +158,6 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements } try { - final String ipAddress = InetAddress.getLocalHost().getHostAddress(); HibernateCallback callback = new HibernateCallback() { public Object doInHibernate(Session session) @@ -994,16 +1003,46 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements /* * Queries for transactions */ + private static final String QUERY_GET_LAST_TXN_ID = "txn.GetLastTxnId"; private static final String QUERY_GET_LAST_TXN_ID_FOR_STORE = "txn.GetLastTxnIdForStore"; private static final String QUERY_GET_TXN_UPDATE_COUNT_FOR_STORE = "txn.GetTxnUpdateCountForStore"; private static final String QUERY_GET_TXN_DELETE_COUNT_FOR_STORE = "txn.GetTxnDeleteCountForStore"; private static final String QUERY_COUNT_TRANSACTIONS = "txn.CountTransactions"; private static final String QUERY_GET_NEXT_TXNS = "txn.GetNextTxns"; + private static final String QUERY_GET_NEXT_REMOTE_TXNS = "txn.GetNextRemoteTxns"; private static final String QUERY_GET_TXN_CHANGES_FOR_STORE = "txn.GetTxnChangesForStore"; private static final String QUERY_GET_TXN_CHANGES = "txn.GetTxnChanges"; + public Transaction getTxnById(long txnId) + { + return (Transaction) getSession().get(TransactionImpl.class, new Long(txnId)); + } + @SuppressWarnings("unchecked") - public Transaction getLastTxn(final StoreRef storeRef) + public Transaction getLastTxn() + { + HibernateCallback callback = new HibernateCallback() + { + public Object doInHibernate(Session session) + { + Query query = session.getNamedQuery(QUERY_GET_LAST_TXN_ID); + query.setMaxResults(1) + .setReadOnly(true); + return query.uniqueResult(); + } + }; + Long txnId = (Long) getHibernateTemplate().execute(callback); + Transaction txn = null; + if (txnId != null) + { + txn = (Transaction) getSession().get(TransactionImpl.class, txnId); + } + // done + return txn; + } + + @SuppressWarnings("unchecked") + public Transaction getLastTxnForStore(final StoreRef storeRef) { HibernateCallback callback = new HibernateCallback() { @@ -1028,7 +1067,7 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements } @SuppressWarnings("unchecked") - public int getTxnUpdateCountForStore(final StoreRef storeRef, final long txnId) + public int getTxnUpdateCount(final long txnId) { HibernateCallback callback = new HibernateCallback() { @@ -1036,9 +1075,6 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements { Query query = session.getNamedQuery(QUERY_GET_TXN_UPDATE_COUNT_FOR_STORE); query.setLong("txnId", txnId) - .setString("protocol", storeRef.getProtocol()) - .setString("identifier", storeRef.getIdentifier()) - .setMaxResults(1) .setReadOnly(true); return query.uniqueResult(); } @@ -1049,7 +1085,7 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements } @SuppressWarnings("unchecked") - public int getTxnDeleteCountForStore(final StoreRef storeRef, final long txnId) + public int getTxnDeleteCount(final long txnId) { HibernateCallback callback = new HibernateCallback() { @@ -1057,9 +1093,6 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements { Query query = session.getNamedQuery(QUERY_GET_TXN_DELETE_COUNT_FOR_STORE); query.setLong("txnId", txnId) - .setString("protocol", storeRef.getProtocol()) - .setString("identifier", storeRef.getIdentifier()) - .setMaxResults(1) .setReadOnly(true); return query.uniqueResult(); } @@ -1088,14 +1121,12 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements } @SuppressWarnings("unchecked") - public List getNextTxns(final Transaction lastTxn, final int count) + public List getNextTxns(final long lastTxnId, final int count) { HibernateCallback callback = new HibernateCallback() { public Object doInHibernate(Session session) { - long lastTxnId = (lastTxn == null) ? -1L : lastTxn.getId(); - Query query = session.getNamedQuery(QUERY_GET_NEXT_TXNS); query.setLong("lastTxnId", lastTxnId) .setMaxResults(count) @@ -1108,6 +1139,26 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements return results; } + @SuppressWarnings("unchecked") + public List getNextRemoteTxns(final long lastTxnId, final int count) + { + HibernateCallback callback = new HibernateCallback() + { + public Object doInHibernate(Session session) + { + Query query = session.getNamedQuery(QUERY_GET_NEXT_REMOTE_TXNS); + query.setLong("lastTxnId", lastTxnId) + .setString("serverIpAddress", ipAddress) + .setMaxResults(count) + .setReadOnly(true); + return query.list(); + } + }; + List results = (List) getHibernateTemplate().execute(callback); + // done + return results; + } + @SuppressWarnings("unchecked") public List getTxnChangesForStore(final StoreRef storeRef, final long txnId) { diff --git a/source/java/org/alfresco/repo/node/index/AbstractReindexComponent.java b/source/java/org/alfresco/repo/node/index/AbstractReindexComponent.java index c715264264..2d4f597540 100644 --- a/source/java/org/alfresco/repo/node/index/AbstractReindexComponent.java +++ b/source/java/org/alfresco/repo/node/index/AbstractReindexComponent.java @@ -16,20 +16,30 @@ */ package org.alfresco.repo.node.index; +import java.util.List; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; import net.sf.acegisecurity.Authentication; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.domain.Transaction; import org.alfresco.repo.node.db.NodeDaoService; import org.alfresco.repo.search.Indexer; +import org.alfresco.repo.search.impl.lucene.LuceneQueryParser; import org.alfresco.repo.search.impl.lucene.fts.FullTextSearchIndexer; import org.alfresco.repo.security.authentication.AuthenticationComponent; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.transaction.TransactionComponent; import org.alfresco.repo.transaction.TransactionUtil; import org.alfresco.repo.transaction.TransactionUtil.TransactionWork; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.NodeRef.Status; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchParameters; import org.alfresco.service.cmr.search.SearchService; import org.alfresco.util.PropertyCheck; import org.alfresco.util.VmShutdownListener; @@ -224,4 +234,229 @@ public abstract class AbstractReindexComponent implements IndexRecovery } } } + + /** + * Gets the last indexed transaction working back from the provided index. + * This method can be used to hunt for a starting point for indexing of + * transactions not yet in the index. + */ + protected long getLastIndexedTxn(long lastTxnId) + { + // get the last transaction + long lastFoundTxnId = lastTxnId + 10L; + boolean found = false; + while (!found && lastFoundTxnId >= 0) + { + // reduce the transaction ID + lastFoundTxnId = lastFoundTxnId - 10L; + // break out as soon as we find a transaction that is in the index + found = isTxnIdPresentInIndex(lastFoundTxnId); + if (found) + { + break; + } + } + // done + if (logger.isDebugEnabled()) + { + logger.debug("Found last index txn before " + lastTxnId + ": " + lastFoundTxnId); + } + return lastFoundTxnId; + } + + protected boolean isTxnIdPresentInIndex(long txnId) + { + if (logger.isDebugEnabled()) + { + logger.debug("Checking for transaction in index: " + txnId); + } + + Transaction txn = nodeDaoService.getTxnById(txnId); + if (txn == null) + { + return true; + } + + // count the changes in the transaction + int updateCount = nodeDaoService.getTxnUpdateCount(txnId); + int deleteCount = nodeDaoService.getTxnDeleteCount(txnId); + if (logger.isDebugEnabled()) + { + logger.debug("Transaction has " + updateCount + " updates and " + deleteCount + " deletes: " + txnId); + } + + // get the stores + boolean found = false; + List storeRefs = nodeService.getStores(); + for (StoreRef storeRef : storeRefs) + { + boolean inStore = isTxnIdPresentInIndex(storeRef, txn, updateCount, deleteCount); + if (inStore) + { + // found in a particular store + found = true; + break; + } + } + // done + if (logger.isDebugEnabled()) + { + logger.debug("Transaction " + txnId + " was " + (found ? "found" : "not found") + " in indexes."); + } + return found; + } + + /** + * @return Returns true if the given transaction is indexed in the in the + */ + private boolean isTxnIdPresentInIndex(StoreRef storeRef, Transaction txn, int updateCount, int deleteCount) + { + long txnId = txn.getId(); + String changeTxnId = txn.getChangeTxnId(); + // do the most update check, which is most common + if (updateCount > 0) + { + ResultSet results = null; + try + { + SearchParameters sp = new SearchParameters(); + sp.addStore(storeRef); + // search for it in the index, sorting with youngest first, fetching only 1 + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery("TX:" + LuceneQueryParser.escape(changeTxnId)); + sp.setLimit(1); + + results = searcher.query(sp); + + if (results.length() > 0) + { + if (logger.isDebugEnabled()) + { + logger.debug("Index has results for txn (OK): " + txnId); + } + return true; // there were updates/creates and results for the txn were found + } + else + { + if (logger.isDebugEnabled()) + { + logger.debug("Index has no results for txn (Index out of date): " + txnId); + } + return false; + } + } + finally + { + if (results != null) { results.close(); } + } + } + // there have been deletes, so we have to ensure that none of the nodes deleted are present in the index + // get all node refs for the transaction + List nodeRefs = nodeDaoService.getTxnChangesForStore(storeRef, txnId); + for (NodeRef nodeRef : nodeRefs) + { + if (logger.isDebugEnabled()) + { + logger.debug("Searching for node in index: \n" + + " node: " + nodeRef + "\n" + + " txn: " + txnId); + } + // we know that these are all deletions + ResultSet results = null; + try + { + SearchParameters sp = new SearchParameters(); + sp.addStore(storeRef); + // search for it in the index, sorting with youngest first, fetching only 1 + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery("ID:" + LuceneQueryParser.escape(nodeRef.toString())); + sp.setLimit(1); + + results = searcher.query(sp); + + if (results.length() == 0) + { + // no results, as expected + if (logger.isDebugEnabled()) + { + logger.debug(" --> Node not found (OK)"); + } + continue; + } + else + { + if (logger.isDebugEnabled()) + { + logger.debug(" --> Node found (Index out of date)"); + } + return false; + } + } + finally + { + if (results != null) { results.close(); } + } + } + + // all tests passed + if (logger.isDebugEnabled()) + { + logger.debug("Index is in synch with transaction: " + txnId); + } + return true; + } + + /** + * Perform a full reindexing of the given transaction in the context of a completely + * new transaction. + * + * @param txnId the transaction identifier + */ + protected void reindexTransaction(final long txnId) + { + if (logger.isDebugEnabled()) + { + logger.debug("Reindexing transaction: " + txnId); + } + + TransactionWork reindexWork = new TransactionWork() + { + public Object doWork() throws Exception + { + // get the node references pertinent to the transaction + List nodeRefs = nodeDaoService.getTxnChanges(txnId); + // reindex each node + for (NodeRef nodeRef : nodeRefs) + { + Status nodeStatus = nodeService.getNodeStatus(nodeRef); + if (nodeStatus == null) + { + // it's not there any more + continue; + } + if (nodeStatus.isDeleted()) // node deleted + { + // only the child node ref is relevant + ChildAssociationRef assocRef = new ChildAssociationRef( + ContentModel.ASSOC_CHILDREN, + null, + null, + nodeRef); + indexer.deleteNode(assocRef); + } + else // node created + { + // get the primary assoc for the node + ChildAssociationRef primaryAssocRef = nodeService.getPrimaryParent(nodeRef); + // reindex + indexer.createNode(primaryAssocRef); + } + } + // done + return null; + } + }; + TransactionUtil.executeInNonPropagatingUserTransaction(transactionService, reindexWork, true); + // done + } } \ No newline at end of file diff --git a/source/java/org/alfresco/repo/node/index/FullIndexRecoveryComponent.java b/source/java/org/alfresco/repo/node/index/FullIndexRecoveryComponent.java index e67c1648bf..f7fc4047f1 100644 --- a/source/java/org/alfresco/repo/node/index/FullIndexRecoveryComponent.java +++ b/source/java/org/alfresco/repo/node/index/FullIndexRecoveryComponent.java @@ -21,27 +21,25 @@ import java.util.List; import org.alfresco.i18n.I18NUtil; import org.alfresco.model.ContentModel; import org.alfresco.repo.domain.Transaction; -import org.alfresco.repo.search.impl.lucene.LuceneQueryParser; import org.alfresco.repo.transaction.TransactionUtil; import org.alfresco.repo.transaction.TransactionUtil.TransactionWork; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.service.cmr.repository.NodeRef.Status; -import org.alfresco.service.cmr.search.ResultSet; -import org.alfresco.service.cmr.search.SearchParameters; -import org.alfresco.service.cmr.search.SearchService; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** - * Component to check and recover the indexes. + * Component to check and recover the indexes. By default, the server is + * put into read-only mode during the reindex process in order to prevent metadata changes. + * This is not critical and can be {@link #setLockServer(boolean) switched off} if the + * server is required immediately. * * @author Derek Hulley */ public class FullIndexRecoveryComponent extends AbstractReindexComponent { - private static final String ERR_STORE_NOT_UP_TO_DATE = "index.recovery.store_not_up_to_date"; + private static final String ERR_INDEX_OUT_OF_DATE = "index.recovery.out_of_date"; private static final String MSG_RECOVERY_STARTING = "index.recovery.starting"; private static final String MSG_RECOVERY_COMPLETE = "index.recovery.complete"; private static final String MSG_RECOVERY_PROGRESS = "index.recovery.progress"; @@ -51,17 +49,25 @@ public class FullIndexRecoveryComponent extends AbstractReindexComponent public static enum RecoveryMode { - /** Do nothing - not even a check */ + /** Do nothing - not even a check. */ NONE, - /** Perform a quick check on the state of the indexes only */ + /** + * Perform a quick check on the state of the indexes only. + */ VALIDATE, - /** Performs a quick validation and then starts a full pass-through on failure */ + /** + * Performs a validation and starts a quick recovery, if necessary. + */ AUTO, - /** Performs a full pass-through of all recorded transactions to ensure that the indexes are up to date */ + /** + * Performs a full pass-through of all recorded transactions to ensure that the indexes + * are up to date. + */ FULL; } private RecoveryMode recoveryMode; + private boolean lockServer; public FullIndexRecoveryComponent() { @@ -69,7 +75,8 @@ public class FullIndexRecoveryComponent extends AbstractReindexComponent } /** - * Set the type of recovery to perform. + * Set the type of recovery to perform. Default is {@link RecoveryMode#VALIDATE to validate} + * the indexes only. * * @param recoveryMode one of the {@link RecoveryMode } values */ @@ -77,7 +84,18 @@ public class FullIndexRecoveryComponent extends AbstractReindexComponent { this.recoveryMode = RecoveryMode.valueOf(recoveryMode); } - + + /** + * Set this on to put the server into READ-ONLY mode for the duration of the index recovery. + * The default is true, i.e. the server will be locked against further updates. + * + * @param lockServer true to force the server to be read-only + */ + public void setLockServer(boolean lockServer) + { + this.lockServer = lockServer; + } + @Override protected void reindexImpl() { @@ -99,25 +117,22 @@ public class FullIndexRecoveryComponent extends AbstractReindexComponent } else // validate first { - List storeRefs = nodeService.getStores(); - for (StoreRef storeRef : storeRefs) + Transaction txn = nodeDaoService.getLastTxn(); + if (txn == null) { - // get the last txn ID in the database - Transaction txn = nodeDaoService.getLastTxn(storeRef); - boolean lastChangeTxnIdInIndex = isTxnIdPresentInIndex(storeRef, txn); - if (lastChangeTxnIdInIndex) - { - // this store is good - continue; - } - // this store isn't up to date - String msg = I18NUtil.getMessage(ERR_STORE_NOT_UP_TO_DATE, storeRef); + // no transactions - just bug out + return; + } + long txnId = txn.getId(); + boolean txnInIndex = isTxnIdPresentInIndex(txnId); + if (!txnInIndex) + { + String msg = I18NUtil.getMessage(ERR_INDEX_OUT_OF_DATE); logger.warn(msg); - // the store is out of date - validation failed + // this store isn't up to date if (recoveryMode == RecoveryMode.VALIDATE) { - // next store - continue; + // the store is out of date - validation failed } else if (recoveryMode == RecoveryMode.AUTO) { @@ -130,8 +145,11 @@ public class FullIndexRecoveryComponent extends AbstractReindexComponent boolean allowWrite = !transactionService.isReadOnly(); try { - // set the server into read-only mode - transactionService.setAllowWrite(false); + if (lockServer) + { + // set the server into read-only mode + transactionService.setAllowWrite(false); + } // do we need to perform a full recovery if (fullRecoveryRequired) @@ -160,8 +178,9 @@ public class FullIndexRecoveryComponent extends AbstractReindexComponent Transaction lastTxn = null; while(true) { + long lastTxnId = (lastTxn == null) ? -1L : lastTxn.getId().longValue(); List nextTxns = nodeDaoService.getNextTxns( - lastTxn, + lastTxnId, MAX_TRANSACTIONS_PER_ITERATION); // reindex each transaction @@ -256,125 +275,4 @@ public class FullIndexRecoveryComponent extends AbstractReindexComponent TransactionUtil.executeInNonPropagatingUserTransaction(transactionService, reindexWork, true); // done } - - private boolean isTxnIdPresentInIndex(StoreRef storeRef, Transaction txn) - { - if (logger.isDebugEnabled()) - { - logger.debug("Checking for transaction in index: \n" + - " store: " + storeRef + "\n" + - " txn: " + txn); - } - - String changeTxnId = txn.getChangeTxnId(); - // count the changes in the transaction - int updateCount = nodeDaoService.getTxnUpdateCountForStore(storeRef, txn.getId()); - int deleteCount = nodeDaoService.getTxnDeleteCountForStore(storeRef, txn.getId()); - if (logger.isDebugEnabled()) - { - logger.debug("Transaction has " + updateCount + " updates and " + deleteCount + " deletes: " + txn); - } - - // do the most update check, which is most common - if (deleteCount == 0 && updateCount == 0) - { - if (logger.isDebugEnabled()) - { - logger.debug("No changes in transaction: " + txn); - } - // there's nothing to check for - return true; - } - else if (updateCount > 0) - { - ResultSet results = null; - try - { - SearchParameters sp = new SearchParameters(); - sp.addStore(storeRef); - // search for it in the index, sorting with youngest first, fetching only 1 - sp.setLanguage(SearchService.LANGUAGE_LUCENE); - sp.setQuery("TX:" + LuceneQueryParser.escape(changeTxnId)); - sp.setLimit(1); - - results = searcher.query(sp); - - if (results.length() > 0) - { - if (logger.isDebugEnabled()) - { - logger.debug("Index has results for txn (OK): " + txn); - } - return true; // there were updates/creates and results for the txn were found - } - else - { - if (logger.isDebugEnabled()) - { - logger.debug("Index has no results for txn (Index out of date): " + txn); - } - return false; - } - } - finally - { - if (results != null) { results.close(); } - } - } - // there have been deletes, so we have to ensure that none of the nodes deleted are present in the index - // get all node refs for the transaction - Long txnId = txn.getId(); - List nodeRefs = nodeDaoService.getTxnChangesForStore(storeRef, txnId); - for (NodeRef nodeRef : nodeRefs) - { - if (logger.isDebugEnabled()) - { - logger.debug("Searching for node in index: \n" + - " node: " + nodeRef + "\n" + - " txn: " + txn); - } - // we know that these are all deletions - ResultSet results = null; - try - { - SearchParameters sp = new SearchParameters(); - sp.addStore(storeRef); - // search for it in the index, sorting with youngest first, fetching only 1 - sp.setLanguage(SearchService.LANGUAGE_LUCENE); - sp.setQuery("ID:" + LuceneQueryParser.escape(nodeRef.toString())); - sp.setLimit(1); - - results = searcher.query(sp); - - if (results.length() == 0) - { - // no results, as expected - if (logger.isDebugEnabled()) - { - logger.debug(" --> Node not found (OK)"); - } - continue; - } - else - { - if (logger.isDebugEnabled()) - { - logger.debug(" --> Node found (Index out of date)"); - } - return false; - } - } - finally - { - if (results != null) { results.close(); } - } - } - - // all tests passed - if (logger.isDebugEnabled()) - { - logger.debug("Index is in synch with transaction: " + txn); - } - return true; - } } \ No newline at end of file diff --git a/source/java/org/alfresco/repo/node/index/IndexRemoteTransactionTracker.java b/source/java/org/alfresco/repo/node/index/IndexRemoteTransactionTracker.java new file mode 100644 index 0000000000..5f0c49e95a --- /dev/null +++ b/source/java/org/alfresco/repo/node/index/IndexRemoteTransactionTracker.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2005-2006 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node.index; + +import java.util.List; + +import org.alfresco.repo.domain.Transaction; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Component to check and recover the indexes. + * + * @author Derek Hulley + */ +public class IndexRemoteTransactionTracker extends AbstractReindexComponent +{ + private static Log logger = LogFactory.getLog(IndexRemoteTransactionTracker.class); + + private boolean remoteOnly; + private long currentTxnId; + + public IndexRemoteTransactionTracker() + { + remoteOnly = true; + currentTxnId = -1L; + } + + /** + * Set whether or not this component should only track remote transactions. + * By default, it is true, but under certain test conditions, it may + * be desirable to track local transactions too; e.g. during testing of clustering + * when running multiple instances on the same machine. + * + * @param remoteOnly true to reindex only those transactions that were + * committed to the database by a remote server. + */ + public void setRemoteOnly(boolean remoteOnly) + { + this.remoteOnly = remoteOnly; + } + + + + @Override + protected void reindexImpl() + { + if (currentTxnId < 0) + { + // initialize the starting point + Transaction lastTxn = nodeDaoService.getLastTxn(); + if (lastTxn == null) + { + // there is nothing to do + return; + } + long lastTxnId = lastTxn.getId(); + currentTxnId = getLastIndexedTxn(lastTxnId); + } + + if (logger.isDebugEnabled()) + { + logger.debug("Performing index tracking from txn " + currentTxnId); + } + + while (true) + { + // get next transactions to index + List txns = getNextTransactions(currentTxnId); + if (txns.size() == 0) + { + // we've caught up + break; + } + // break out if the VM is shutting down + if (isShuttingDown()) + { + break; + } + // reindex all "foreign" or "local" transactions, one at a time + for (Transaction txn : txns) + { + long txnId = txn.getId(); + reindexTransaction(txnId); + currentTxnId = txnId; + } + } + } + + private static final int MAX_TXN_COUNT = 1000; + private List getNextTransactions(long currentTxnId) + { + List txns = null; + if (remoteOnly) + { + txns = nodeDaoService.getNextRemoteTxns(currentTxnId, MAX_TXN_COUNT); + } + else + { + txns = nodeDaoService.getNextTxns(currentTxnId, MAX_TXN_COUNT); + } + // done + return txns; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/node/index/IndexRemoteTransactionTrackerTest.java b/source/java/org/alfresco/repo/node/index/IndexRemoteTransactionTrackerTest.java new file mode 100644 index 0000000000..a33ff0bf40 --- /dev/null +++ b/source/java/org/alfresco/repo/node/index/IndexRemoteTransactionTrackerTest.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2005-2006 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.node.index; + +import junit.framework.TestCase; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.ContentStore; +import org.alfresco.repo.node.db.NodeDaoService; +import org.alfresco.repo.search.Indexer; +import org.alfresco.repo.search.impl.lucene.fts.FullTextSearchIndexer; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.repo.transaction.TransactionComponent; +import org.alfresco.repo.transaction.TransactionUtil; +import org.alfresco.repo.transaction.TransactionUtil.TransactionWork; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ApplicationContext; + +/** + * @see org.alfresco.repo.node.index.IndexRemoteTransactionTracker + * + * @author Derek Hulley + */ +@SuppressWarnings("unused") +public class IndexRemoteTransactionTrackerTest extends TestCase +{ + private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + + private AuthenticationComponent authenticationComponent; + private SearchService searchService; + private NodeService nodeService; + private FileFolderService fileFolderService; + private ContentStore contentStore; + private FullTextSearchIndexer ftsIndexer; + private Indexer indexer; + private NodeRef rootNodeRef; + + private IndexRemoteTransactionTracker indexTracker; + + public void setUp() throws Exception + { + ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); + searchService = serviceRegistry.getSearchService(); + nodeService = serviceRegistry.getNodeService(); + fileFolderService = serviceRegistry.getFileFolderService(); + authenticationComponent = (AuthenticationComponent) ctx.getBean("authenticationComponentImpl"); + contentStore = (ContentStore) ctx.getBean("fileContentStore"); + ftsIndexer = (FullTextSearchIndexer) ctx.getBean("LuceneFullTextSearchIndexer"); + + indexer = (Indexer) ctx.getBean("indexerComponent"); + NodeDaoService nodeDaoService = (NodeDaoService) ctx.getBean("nodeDaoService"); + TransactionService transactionService = serviceRegistry.getTransactionService(); + indexTracker = new IndexRemoteTransactionTracker(); + indexTracker.setAuthenticationComponent(authenticationComponent); + indexTracker.setFtsIndexer(ftsIndexer); + indexTracker.setIndexer(indexer); + indexTracker.setNodeDaoService(nodeDaoService); + indexTracker.setNodeService(nodeService); + indexTracker.setSearcher(searchService); + indexTracker.setTransactionComponent((TransactionComponent)transactionService); + + // authenticate + authenticationComponent.setSystemUserAsCurrentUser(); + + // disable indexing + TransactionWork createNodeWork = new TransactionWork() + { + public ChildAssociationRef doWork() throws Exception + { + StoreRef storeRef = new StoreRef("test", getName() + "-" + System.currentTimeMillis()); + NodeRef rootNodeRef = null; + if (!nodeService.exists(storeRef)) + { + nodeService.createStore(storeRef.getProtocol(), storeRef.getIdentifier()); + } + rootNodeRef = nodeService.getRootNode(storeRef); + // create another node + ChildAssociationRef childAssocRef = nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(NamespaceService.ALFRESCO_URI, "xyz"), + ContentModel.TYPE_FOLDER); + // remove the node from the index + indexer.deleteNode(childAssocRef); + return childAssocRef; + } + }; + ChildAssociationRef childAssocRef = TransactionUtil.executeInUserTransaction(transactionService, createNodeWork); + } + + public void testSetup() throws Exception + { + + } + + public synchronized void testStartup() throws Exception + { + indexTracker.reindex(); + indexTracker.reindex(); + } +} diff --git a/source/java/org/alfresco/repo/rule/BaseRuleTest.java b/source/java/org/alfresco/repo/rule/BaseRuleTest.java index 332f0495dd..d15d1ea7d6 100644 --- a/source/java/org/alfresco/repo/rule/BaseRuleTest.java +++ b/source/java/org/alfresco/repo/rule/BaseRuleTest.java @@ -118,7 +118,8 @@ public class BaseRuleTest extends BaseSpringTest this.transactionService = (TransactionService)this.applicationContext.getBean("transactionComponent"); this.authenticationComponent = (AuthenticationComponent)this.applicationContext.getBean("authenticationComponent"); - authenticationComponent.setSystemUserAsCurrentUser(); + //authenticationComponent.setSystemUserAsCurrentUser(); + authenticationComponent.setCurrentUser("admin"); // Get the rule type this.ruleType = this.ruleService.getRuleType(RULE_TYPE_NAME); diff --git a/source/java/org/alfresco/repo/rule/RuleServiceCoverageTest.java b/source/java/org/alfresco/repo/rule/RuleServiceCoverageTest.java index c00ed1e840..0374d8092e 100644 --- a/source/java/org/alfresco/repo/rule/RuleServiceCoverageTest.java +++ b/source/java/org/alfresco/repo/rule/RuleServiceCoverageTest.java @@ -149,7 +149,8 @@ public class RuleServiceCoverageTest extends TestCase this.authenticationComponent = (AuthenticationComponent)applicationContext.getBean("authenticationComponent"); //authenticationComponent.setCurrentUser(authenticationComponent.getSystemUserName()); - authenticationComponent.setSystemUserAsCurrentUser(); + //authenticationComponent.setSystemUserAsCurrentUser(); + authenticationComponent.setCurrentUser("admin"); this.testStoreRef = this.nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); this.rootNodeRef = this.nodeService.getRootNode(this.testStoreRef); diff --git a/source/java/org/alfresco/repo/rule/RuleServiceImpl.java b/source/java/org/alfresco/repo/rule/RuleServiceImpl.java index 9bf1eea747..6d0b9420b7 100644 --- a/source/java/org/alfresco/repo/rule/RuleServiceImpl.java +++ b/source/java/org/alfresco/repo/rule/RuleServiceImpl.java @@ -40,6 +40,8 @@ import org.alfresco.service.cmr.rule.Rule; import org.alfresco.service.cmr.rule.RuleService; import org.alfresco.service.cmr.rule.RuleServiceException; import org.alfresco.service.cmr.rule.RuleType; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.RegexQNamePattern; import org.alfresco.util.GUID; @@ -92,6 +94,11 @@ public class RuleServiceImpl implements RuleService, RuntimeRuleService */ private DictionaryService dictionaryService; + /** + * The permission service + */ + private PermissionService permissionService; + /** * The action service implementation which we need for some things. */ @@ -179,6 +186,16 @@ public class RuleServiceImpl implements RuleService, RuntimeRuleService this.dictionaryService = dictionaryService; } + /** + * Set the permission service + * + * @param permissionService the permission service + */ + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } + /** * Set the global rules disabled flag * @@ -572,49 +589,56 @@ public class RuleServiceImpl implements RuleService, RuntimeRuleService */ public void saveRule(NodeRef nodeRef, Rule rule) { - disableRules(); - try - { - if (this.nodeService.exists(nodeRef) == false) - { - throw new RuleServiceException("The node does not exist."); - } - - NodeRef ruleNodeRef = rule.getNodeRef(); - if (ruleNodeRef == null) - { - if (this.nodeService.hasAspect(nodeRef, RuleModel.ASPECT_RULES) == false) - { - // Add the actionable aspect - this.nodeService.addAspect(nodeRef, RuleModel.ASPECT_RULES, null); - } - - // Create the action node - ruleNodeRef = this.nodeService.createNode( - getSavedRuleFolderRef(nodeRef), - ContentModel.ASSOC_CONTAINS, - QName.createQName(RuleModel.RULE_MODEL_URI, ASSOC_NAME_RULES_PREFIX + GUID.generate()), - RuleModel.TYPE_RULE).getChildRef(); - - // Set the rule node reference and the owning node reference - rule.setNodeRef(ruleNodeRef); - } - - // Update the properties of the rule - this.nodeService.setProperty(ruleNodeRef, ContentModel.PROP_TITLE, rule.getTitle()); - this.nodeService.setProperty(ruleNodeRef, ContentModel.PROP_DESCRIPTION, rule.getDescription()); - this.nodeService.setProperty(ruleNodeRef, RuleModel.PROP_RULE_TYPE, (Serializable)rule.getRuleTypes()); - this.nodeService.setProperty(ruleNodeRef, RuleModel.PROP_APPLY_TO_CHILDREN, rule.isAppliedToChildren()); - this.nodeService.setProperty(ruleNodeRef, RuleModel.PROP_EXECUTE_ASYNC, rule.getExecuteAsynchronously()); - this.nodeService.setProperty(ruleNodeRef, RuleModel.PROP_DISABLED, rule.getRuleDisabled()); - - // Save the rule's action - saveAction(ruleNodeRef, rule); - } - finally - { - enableRules(); - } + if (this.permissionService.hasPermission(nodeRef, PermissionService.CHANGE_PERMISSIONS) == AccessStatus.ALLOWED) + { + disableRules(); + try + { + if (this.nodeService.exists(nodeRef) == false) + { + throw new RuleServiceException("The node does not exist."); + } + + NodeRef ruleNodeRef = rule.getNodeRef(); + if (ruleNodeRef == null) + { + if (this.nodeService.hasAspect(nodeRef, RuleModel.ASPECT_RULES) == false) + { + // Add the actionable aspect + this.nodeService.addAspect(nodeRef, RuleModel.ASPECT_RULES, null); + } + + // Create the action node + ruleNodeRef = this.nodeService.createNode( + getSavedRuleFolderRef(nodeRef), + ContentModel.ASSOC_CONTAINS, + QName.createQName(RuleModel.RULE_MODEL_URI, ASSOC_NAME_RULES_PREFIX + GUID.generate()), + RuleModel.TYPE_RULE).getChildRef(); + + // Set the rule node reference and the owning node reference + rule.setNodeRef(ruleNodeRef); + } + + // Update the properties of the rule + this.nodeService.setProperty(ruleNodeRef, ContentModel.PROP_TITLE, rule.getTitle()); + this.nodeService.setProperty(ruleNodeRef, ContentModel.PROP_DESCRIPTION, rule.getDescription()); + this.nodeService.setProperty(ruleNodeRef, RuleModel.PROP_RULE_TYPE, (Serializable)rule.getRuleTypes()); + this.nodeService.setProperty(ruleNodeRef, RuleModel.PROP_APPLY_TO_CHILDREN, rule.isAppliedToChildren()); + this.nodeService.setProperty(ruleNodeRef, RuleModel.PROP_EXECUTE_ASYNC, rule.getExecuteAsynchronously()); + this.nodeService.setProperty(ruleNodeRef, RuleModel.PROP_DISABLED, rule.getRuleDisabled()); + + // Save the rule's action + saveAction(ruleNodeRef, rule); + } + finally + { + enableRules(); + } + } + else + { + throw new RuleServiceException("Insufficient permissions to save a rule."); + } } /** @@ -667,22 +691,29 @@ public class RuleServiceImpl implements RuleService, RuntimeRuleService */ public void removeRule(NodeRef nodeRef, Rule rule) { - if (this.nodeService.exists(nodeRef) == true && - this.nodeService.hasAspect(nodeRef, RuleModel.ASPECT_RULES) == true) + if (this.permissionService.hasPermission(nodeRef, PermissionService.CHANGE_PERMISSIONS) == AccessStatus.ALLOWED) { - disableRules(nodeRef); - try - { - NodeRef ruleNodeRef = rule.getNodeRef(); - if (ruleNodeRef != null) - { - this.nodeService.removeChild(getSavedRuleFolderRef(nodeRef), ruleNodeRef); - } - } - finally - { - enableRules(nodeRef); - } + if (this.nodeService.exists(nodeRef) == true && + this.nodeService.hasAspect(nodeRef, RuleModel.ASPECT_RULES) == true) + { + disableRules(nodeRef); + try + { + NodeRef ruleNodeRef = rule.getNodeRef(); + if (ruleNodeRef != null) + { + this.nodeService.removeChild(getSavedRuleFolderRef(nodeRef), ruleNodeRef); + } + } + finally + { + enableRules(nodeRef); + } + } + } + else + { + throw new RuleServiceException("Insufficient permissions to remove a rule."); } } @@ -691,20 +722,27 @@ public class RuleServiceImpl implements RuleService, RuntimeRuleService */ public void removeAllRules(NodeRef nodeRef) { - if (this.nodeService.exists(nodeRef) == true && - this.nodeService.hasAspect(nodeRef, RuleModel.ASPECT_RULES) == true) + if (this.permissionService.hasPermission(nodeRef, PermissionService.CHANGE_PERMISSIONS) == AccessStatus.ALLOWED) { - NodeRef folder = getSavedRuleFolderRef(nodeRef); - if (folder != null) - { - List ruleChildAssocs = this.nodeService.getChildAssocs( - folder, - RegexQNamePattern.MATCH_ALL, ASSOC_NAME_RULES_REGEX); - for (ChildAssociationRef ruleChildAssoc : ruleChildAssocs) - { - this.nodeService.removeChild(folder, ruleChildAssoc.getChildRef()); - } - } + if (this.nodeService.exists(nodeRef) == true && + this.nodeService.hasAspect(nodeRef, RuleModel.ASPECT_RULES) == true) + { + NodeRef folder = getSavedRuleFolderRef(nodeRef); + if (folder != null) + { + List ruleChildAssocs = this.nodeService.getChildAssocs( + folder, + RegexQNamePattern.MATCH_ALL, ASSOC_NAME_RULES_REGEX); + for (ChildAssociationRef ruleChildAssoc : ruleChildAssocs) + { + this.nodeService.removeChild(folder, ruleChildAssoc.getChildRef()); + } + } + } + } + else + { + throw new RuleServiceException("Insufficient permissions to remove a rule."); } } diff --git a/source/java/org/alfresco/repo/rule/RuleServiceImplTest.java b/source/java/org/alfresco/repo/rule/RuleServiceImplTest.java index 10f34d4820..cdadf1e724 100644 --- a/source/java/org/alfresco/repo/rule/RuleServiceImplTest.java +++ b/source/java/org/alfresco/repo/rule/RuleServiceImplTest.java @@ -27,6 +27,8 @@ import org.alfresco.repo.action.evaluator.ComparePropertyValueEvaluator; import org.alfresco.repo.action.executer.ImageTransformActionExecuter; import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.content.transform.AbstractContentTransformerTest; +import org.alfresco.repo.transaction.TransactionUtil; +import org.alfresco.repo.transaction.TransactionUtil.TransactionWork; import org.alfresco.service.cmr.action.Action; import org.alfresco.service.cmr.action.ActionCondition; import org.alfresco.service.cmr.repository.ContentWriter; @@ -34,7 +36,10 @@ import org.alfresco.service.cmr.repository.CyclicChildRelationshipException; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.rule.Rule; import org.alfresco.service.cmr.rule.RuleType; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.namespace.QName; +import org.apache.commons.digester.SetRootRule; /** @@ -44,7 +49,17 @@ import org.alfresco.service.namespace.QName; */ public class RuleServiceImplTest extends BaseRuleTest { - + AuthenticationService authenticationService; + PermissionService permissionService; + + @Override + protected void onSetUpInTransaction() throws Exception + { + super.onSetUpInTransaction(); + this.permissionService = (PermissionService)this.applicationContext.getBean("permissionService"); + this.authenticationService = (AuthenticationService)this.applicationContext.getBean("authenticationService"); + } + /** * Test get rule type */ @@ -296,6 +311,59 @@ public class RuleServiceImplTest extends BaseRuleTest ContentModel.TYPE_CONTAINER).getChildRef(); } + public void testRuleServicePermissionsConsumer() + { + this.authenticationService.createAuthentication("conUser", "password".toCharArray()); + this.permissionService.setPermission(this.nodeRef, "conUser", PermissionService.CONSUMER, true); + this.permissionService.setInheritParentPermissions(this.nodeRef, true); + + this.authenticationService.authenticate("conUser", "password".toCharArray()); + Rule rule = createTestRule(); + try + { + this.ruleService.saveRule(this.nodeRef, rule); + // Fail + fail("Consumers cannot create rules."); + } + catch (Exception exception) + { + // Ok + } + + } + + public void testRuleServicePermissionsEditor() + { + this.authenticationService.createAuthentication("editorUser", "password".toCharArray()); + this.permissionService.setPermission(this.nodeRef, "editorUser", PermissionService.EDITOR, true); + this.permissionService.setInheritParentPermissions(this.nodeRef, true); + + this.authenticationService.authenticate("editorUser", "password".toCharArray()); + Rule rule = createTestRule(); + try + { + this.ruleService.saveRule(this.nodeRef, rule); + // Fail + fail("Editors cannot create rules."); + } + catch (Exception exception) + { + // Ok + } + } + + public void testRuleServicePermissionsCoordinator() + { + this.authenticationService.createAuthentication("coordUser", "password".toCharArray()); + this.permissionService.setPermission(this.nodeRef, "coordUser", PermissionService.COORDINATOR, true); + this.permissionService.setInheritParentPermissions(this.nodeRef, true); + + this.authenticationService.authenticate("admin", "admin".toCharArray()); + Rule rule2 = createTestRule(); + this.ruleService.saveRule(this.nodeRef, rule2); + this.authenticationService.clearCurrentSecurityContext(); + } + /** * Tests the rule inheritance within the store, checking that the cache is reset correctly when * rules are added and removed. diff --git a/source/java/org/alfresco/repo/search/impl/lucene/LuceneSearcherImpl2.java b/source/java/org/alfresco/repo/search/impl/lucene/LuceneSearcherImpl2.java index 7a9b576895..cf3fa13ae6 100644 --- a/source/java/org/alfresco/repo/search/impl/lucene/LuceneSearcherImpl2.java +++ b/source/java/org/alfresco/repo/search/impl/lucene/LuceneSearcherImpl2.java @@ -49,6 +49,9 @@ import org.alfresco.service.namespace.NamespacePrefixResolver; import org.alfresco.service.namespace.QName; import org.alfresco.util.ISO9075; import org.alfresco.util.SearchLanguageConversion; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermEnum; import org.apache.lucene.search.Hits; import org.apache.lucene.search.Query; import org.apache.lucene.search.Searcher; @@ -230,7 +233,7 @@ public class LuceneSearcherImpl2 extends LuceneBase2 implements LuceneSearcher2 switch (sd.getSortType()) { case FIELD: - if (searcher.getReader().getFieldNames().contains(sd.getField())) + if (fieldHasTerm(searcher.getReader(), sd.getField())) { fields[index++] = new SortField(sd.getField(), !sd.isAscending()); } @@ -308,6 +311,35 @@ public class LuceneSearcherImpl2 extends LuceneBase2 implements LuceneSearcher2 } } + private static boolean fieldHasTerm(IndexReader indexReader, String field) + { + try + { + TermEnum termEnum = indexReader.terms(new Term(field, "")); + try + { + if (termEnum.next()) + { + Term first = termEnum.term(); + return first.field().equals(field); + } + else + { + return false; + } + } + finally + { + termEnum.close(); + } + } + catch (IOException e) + { + throw new SearcherException("Could not find terms for sort field ", e); + } + + } + public ResultSet query(StoreRef store, String language, String query) { return query(store, language, query, null, null); diff --git a/source/java/org/alfresco/repo/security/permissions/dynamic/LockOwnerDynamicAuthority.java b/source/java/org/alfresco/repo/security/permissions/dynamic/LockOwnerDynamicAuthority.java index 1549425147..0713337710 100644 --- a/source/java/org/alfresco/repo/security/permissions/dynamic/LockOwnerDynamicAuthority.java +++ b/source/java/org/alfresco/repo/security/permissions/dynamic/LockOwnerDynamicAuthority.java @@ -16,6 +16,8 @@ */ package org.alfresco.repo.security.permissions.dynamic; +import java.io.Serializable; + import org.alfresco.model.ContentModel; import org.alfresco.repo.security.permissions.DynamicAuthority; import org.alfresco.service.cmr.lock.LockService; @@ -44,9 +46,13 @@ public class LockOwnerDynamicAuthority implements DynamicAuthority, Initializing } if (nodeService.hasAspect(nodeRef, ContentModel.ASPECT_WORKING_COPY)) { - NodeRef original = DefaultTypeConverter.INSTANCE.convert( - NodeRef.class, nodeService.getProperty(nodeRef, ContentModel.PROP_COPY_REFERENCE)); - if (nodeService.exists(original)) + NodeRef original = null; + Serializable reference = nodeService.getProperty(nodeRef, ContentModel.PROP_COPY_REFERENCE); + if (reference != null) + { + original = DefaultTypeConverter.INSTANCE.convert(NodeRef.class, reference); + } + if (original != null && nodeService.exists(original)) { return (lockService.getLockStatus(original) == LockStatus.LOCK_OWNER); } diff --git a/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryVoter.java b/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryVoter.java index fef50dd7f4..18e75a1423 100644 --- a/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryVoter.java +++ b/source/java/org/alfresco/repo/security/permissions/impl/acegi/ACLEntryVoter.java @@ -44,7 +44,6 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.InitializingBean; /** - * * @author andyh */ @@ -153,8 +152,8 @@ public class ACLEntryVoter implements AccessDecisionVoter, InitializingBean if ((attribute.getAttribute() != null) && (attribute.getAttribute().startsWith(ACL_NODE) || attribute.getAttribute().startsWith(ACL_PARENT) - || attribute.getAttribute().startsWith(ACL_ALLOW) - || attribute.getAttribute().startsWith(ACL_METHOD))) + || attribute.getAttribute().startsWith(ACL_ALLOW) || attribute.getAttribute().startsWith( + ACL_METHOD))) { return true; } @@ -165,14 +164,11 @@ public class ACLEntryVoter implements AccessDecisionVoter, InitializingBean } /** - * This implementation supports only MethodSecurityInterceptor, - * because it queries the presented MethodInvocation. + * This implementation supports only MethodSecurityInterceptor, because it queries the presented MethodInvocation. * * @param clazz * the secure object - * - * @return true if the secure object is - * MethodInvocation, false otherwise + * @return true if the secure object is MethodInvocation, false otherwise */ public boolean supports(Class clazz) { @@ -253,7 +249,15 @@ public class ACLEntryVoter implements AccessDecisionVoter, InitializingBean testNodeRef = (NodeRef) invocation.getArguments()[cad.parameter]; if (log.isDebugEnabled()) { - log.debug("\tPermission test on node " + nodeService.getPath(testNodeRef)); + if (nodeService.exists(testNodeRef)) + { + log.debug("\tPermission test on node " + nodeService.getPath(testNodeRef)); + } + else + { + log.debug("\tPermission test on non-existing node " +testNodeRef); + } + } } else if (ChildAssociationRef.class.isAssignableFrom(params[cad.parameter])) @@ -263,7 +267,14 @@ public class ACLEntryVoter implements AccessDecisionVoter, InitializingBean testNodeRef = ((ChildAssociationRef) invocation.getArguments()[cad.parameter]).getChildRef(); if (log.isDebugEnabled()) { - log.debug("\tPermission test on node " + nodeService.getPath(testNodeRef)); + if (nodeService.exists(testNodeRef)) + { + log.debug("\tPermission test on node " + nodeService.getPath(testNodeRef)); + } + else + { + log.debug("\tPermission test on non-existing node " + testNodeRef); + } } } } @@ -284,6 +295,14 @@ public class ACLEntryVoter implements AccessDecisionVoter, InitializingBean testNodeRef = nodeService.getPrimaryParent(child).getParentRef(); if (log.isDebugEnabled()) { + if (nodeService.exists(testNodeRef)) + { + log.debug("\tPermission test for parent on node " + nodeService.getPath(testNodeRef)); + } + else + { + log.debug("\tPermission test for parent on non-existing node " + testNodeRef); + } log.debug("\tPermission test for parent on node " + nodeService.getPath(testNodeRef)); } } @@ -295,8 +314,17 @@ public class ACLEntryVoter implements AccessDecisionVoter, InitializingBean testNodeRef = ((ChildAssociationRef) invocation.getArguments()[cad.parameter]).getParentRef(); if (log.isDebugEnabled()) { - log.debug("\tPermission test for parent on child assoc ref for node " - + nodeService.getPath(testNodeRef)); + if (nodeService.exists(testNodeRef)) + { + log.debug("\tPermission test for parent on child assoc ref for node " + + nodeService.getPath(testNodeRef)); + } + else + { + log.debug("\tPermission test for parent on child assoc ref for non existing node " + + testNodeRef); + } + } } diff --git a/source/java/org/alfresco/repo/template/Classification.java b/source/java/org/alfresco/repo/template/Classification.java new file mode 100644 index 0000000000..3c55a5d0f6 --- /dev/null +++ b/source/java/org/alfresco/repo/template/Classification.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.template; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.alfresco.repo.jscript.CategoryTemplateNode; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.TemplateImageResolver; +import org.alfresco.service.cmr.repository.TemplateNode; +import org.alfresco.service.cmr.search.CategoryService; +import org.alfresco.service.namespace.QName; + +/** + * Support for finding classifications and their root categories. + * + * @author Andy Hind + */ +public final class Classification +{ + private ServiceRegistry services; + private TemplateImageResolver imageResolver; + private StoreRef storeRef; + + public Classification(StoreRef storeRef, ServiceRegistry services, TemplateImageResolver imageResolver) + { + this.storeRef = storeRef; + this.services = services; + this.imageResolver = imageResolver; + } + + /** + * Find all the category nodes in a given classification. + * + * @param aspect + * + * @return all the category nodes in a given classification. + */ + public List getAllCategoryNodes(String aspect) + { + return buildCategoryNodes(services.getCategoryService().getCategories(storeRef, createQName(aspect), + CategoryService.Depth.ANY)); + } + + /** + * Find all the category nodes in a given classification. + * + * @param aspect + * + * @return all the category nodes in a given classification. + */ + public List getAllCategoryNodes(QName aspect) + { + return buildCategoryNodes(services.getCategoryService().getCategories(storeRef, aspect, + CategoryService.Depth.ANY)); + } + + /** + * @return all the aspects that define a classification. + */ + public List getAllClassificationAspects() + { + Collection aspects = services.getCategoryService().getClassificationAspects(); + ArrayList answer = new ArrayList(aspects.size()); + answer.addAll(aspects); + return answer; + } + + /** + * Get the root categories in a classification. + * + * @param aspect + * + * @return List of TemplateNode + */ + public List getRootCategories(String aspect) + { + return buildCategoryNodes(services.getCategoryService().getRootCategories(storeRef, createQName(aspect))); + } + + + private List buildCategoryNodes(Collection cars) + { + ArrayList categoryNodes = new ArrayList(cars.size()); + for (ChildAssociationRef car : cars) + { + categoryNodes.add(new CategoryTemplateNode(car.getChildRef(), this.services, this.imageResolver)); + } + return categoryNodes; + } + + private QName createQName(String s) + { + QName qname; + if (s.indexOf(QName.NAMESPACE_BEGIN) != -1) + { + qname = QName.createQName(s); + } + else + { + qname = QName.createQName(s, this.services.getNamespaceService()); + } + return qname; + } +} diff --git a/source/java/org/alfresco/repo/template/FreeMarkerProcessor.java b/source/java/org/alfresco/repo/template/FreeMarkerProcessor.java index 2ba4994524..6aae071809 100644 --- a/source/java/org/alfresco/repo/template/FreeMarkerProcessor.java +++ b/source/java/org/alfresco/repo/template/FreeMarkerProcessor.java @@ -301,6 +301,13 @@ public class FreeMarkerProcessor implements TemplateProcessor // current date/time is useful to have and isn't supplied by FreeMarker by default model.put("date", new Date()); + // Session support + model.put("session", new Session(services, imageResolver)); + + // Classification support + + model.put("classification", new Classification(companyHome.getStoreRef(), services, imageResolver)); + // add custom method objects model.put("hasAspect", new HasAspectMethod()); model.put("message", new I18NMessageMethod()); diff --git a/source/java/org/alfresco/repo/template/Session.java b/source/java/org/alfresco/repo/template/Session.java new file mode 100644 index 0000000000..e1007de8c9 --- /dev/null +++ b/source/java/org/alfresco/repo/template/Session.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.template; + +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.TemplateImageResolver; + +/** + * Support session information in free marker templates. + * + * @author Andy Hind + */ +public class Session +{ + + private ServiceRegistry services; + + @SuppressWarnings("unused") + private TemplateImageResolver imageResolver; + + public Session(ServiceRegistry services, TemplateImageResolver imageResolver) + { + this.services = services; + this.imageResolver = imageResolver; + } + + /** + * Get the current authentication ticket. + * + * @return + */ + public String getTicket() + { + return services.getAuthenticationService().getCurrentTicket(); + } +} diff --git a/source/java/org/alfresco/repo/version/common/counter/VersionCounterService.java b/source/java/org/alfresco/repo/version/common/counter/VersionCounterService.java index fa41e2e373..c45aeb35ec 100644 --- a/source/java/org/alfresco/repo/version/common/counter/VersionCounterService.java +++ b/source/java/org/alfresco/repo/version/common/counter/VersionCounterService.java @@ -59,4 +59,16 @@ public interface VersionCounterService * @param storeRef the store reference */ public void resetVersionNumber(StoreRef storeRef); + + /** + * Sets the version number for a specified store. + * + * WARNING: calling this method will completely reset the current + * version count for the specified store and cannot be undone. + * + * @param storeRef the store reference + * @param versionCount the new version count + */ + public void setVersionNumber(StoreRef storeRef, int versionCount); + } diff --git a/source/java/org/alfresco/repo/workflow/WorkflowDeployer.java b/source/java/org/alfresco/repo/workflow/WorkflowDeployer.java index 0755746654..a6c3d20d3c 100644 --- a/source/java/org/alfresco/repo/workflow/WorkflowDeployer.java +++ b/source/java/org/alfresco/repo/workflow/WorkflowDeployer.java @@ -31,11 +31,10 @@ import org.alfresco.service.cmr.workflow.WorkflowDeployment; import org.alfresco.service.cmr.workflow.WorkflowException; import org.alfresco.service.cmr.workflow.WorkflowService; import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.AbstractLifecycleBean; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationEvent; -import org.springframework.context.ApplicationListener; -import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.core.io.ClassPathResource; @@ -44,7 +43,7 @@ import org.springframework.core.io.ClassPathResource; * * @author davidc */ -public class WorkflowDeployer implements ApplicationListener +public class WorkflowDeployer extends AbstractLifecycleBean { // Logging support private static Log logger = LogFactory.getLog("org.alfresco.repo.workflow"); @@ -222,16 +221,16 @@ public class WorkflowDeployer implements ApplicationListener } } - /* - * (non-Javadoc) - * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent) - */ - public void onApplicationEvent(ApplicationEvent event) + @Override + protected void onBootstrap(ApplicationEvent event) { - if (event instanceof ContextRefreshedEvent) - { - deploy(); - } + deploy(); + } + + @Override + protected void onShutdown(ApplicationEvent event) + { + // NOOP } } diff --git a/source/java/org/alfresco/repo/workflow/WorkflowInterpreter.java b/source/java/org/alfresco/repo/workflow/WorkflowInterpreter.java new file mode 100644 index 0000000000..90eda03c7d --- /dev/null +++ b/source/java/org/alfresco/repo/workflow/WorkflowInterpreter.java @@ -0,0 +1,707 @@ +/* + * Copyright (C) 2006 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.workflow; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.i18n.I18NUtil; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.workflow.WorkflowDefinition; +import org.alfresco.service.cmr.workflow.WorkflowDeployment; +import org.alfresco.service.cmr.workflow.WorkflowInstance; +import org.alfresco.service.cmr.workflow.WorkflowPath; +import org.alfresco.service.cmr.workflow.WorkflowService; +import org.alfresco.service.cmr.workflow.WorkflowTask; +import org.alfresco.service.cmr.workflow.WorkflowTaskState; +import org.alfresco.service.cmr.workflow.WorkflowTransition; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ApplicationContext; +import org.springframework.core.io.ClassPathResource; + +/** + * An interactive console for Workflows. + * + * @author davidc + */ +public class WorkflowInterpreter +{ + // Service dependencies + private WorkflowService workflowService; + private NamespaceService namespaceService; + private PersonService personService; + + /** + * The reader for interaction. + */ + private BufferedReader fIn; + + /** + * Current context + */ + private WorkflowDefinition currentWorkflowDef = null; + private WorkflowPath currentPath = null; + private String currentDeploy = null; + + /** + * Last command issued + */ + private String lastCommand = null; + + /** + * Variables + */ + private Map vars = new HashMap(); + + + /** + * Main entry point. + * + * Syntax: AVMInteractiveConsole storage (new|old). + */ + public static void main(String[] args) + { + ApplicationContext context = ApplicationContextHelper.getApplicationContext(); + WorkflowInterpreter console = (WorkflowInterpreter)context.getBean("workflowInterpreter"); + AuthenticationUtil.setSystemUserAsCurrentUser(); + console.rep(); + System.exit(0); + } + + /** + * Make up a new console. + */ + public WorkflowInterpreter() + { + fIn = new BufferedReader(new InputStreamReader(System.in)); + } + + /** + * @param workflowService The Workflow Service + */ + public void setWorkflowService(WorkflowService workflowService) + { + this.workflowService = workflowService; + } + + /** + * @param namespaceService namespaceService + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * @param personService personService + */ + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + + /** + * A Read-Eval-Print loop. + */ + public void rep() + { + while (true) + { + System.out.print("ok> "); + try + { + String line = fIn.readLine(); + if (line.equals("exit")) + { + return; + } + long startms = System.currentTimeMillis(); + System.out.print(interpretCommand(line)); + System.out.println("" + (System.currentTimeMillis() - startms) + "ms"); + } + catch (Exception e) + { + e.printStackTrace(System.err); + System.out.println(""); + } + } + } + + /** + * Interpret a single command using the BufferedReader passed in for any data needed. + * + * @param line The unparsed command + * @return The textual output of the command. + */ + public String interpretCommand(String line) + throws IOException + { + String[] command = line.split(" "); + if (command.length == 0) + { + command = new String[1]; + command[0] = line; + } + + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(bout); + + // repeat last command? + if (command[0].equals("r")) + { + if (lastCommand == null) + { + return "No command entered yet."; + } + return "repeating command " + lastCommand + "\n\n" + interpretCommand(lastCommand); + } + + // remember last command + lastCommand = line; + + // execute command + if (command[0].equals("help")) + { + String helpFile = I18NUtil.getMessage("workflow_console.help"); + ClassPathResource helpResource = new ClassPathResource(helpFile); + byte[] helpBytes = new byte[500]; + InputStream helpStream = helpResource.getInputStream(); + try + { + int read = helpStream.read(helpBytes); + while (read != -1) + { + bout.write(helpBytes, 0, read); + read = helpStream.read(helpBytes); + } + } + finally + { + helpStream.close(); + } + } + + else if (command[0].equals("show")) + { + if (command.length < 2) + { + return "Syntax Error.\n"; + } + + else if (command[1].equals("definitions")) + { + List defs = workflowService.getDefinitions(); + for (WorkflowDefinition def : defs) + { + out.println("id: " + def.id + " , name: " + def.name + " , title: " + def.title + " , version: " + def.version); + } + } + + else if (command[1].equals("workflows")) + { + if (currentWorkflowDef == null) + { + return "workflow definition not in use. Enter command use .\n"; + } + List workflows = workflowService.getActiveWorkflows(currentWorkflowDef.id); + for (WorkflowInstance workflow : workflows) + { + out.println("id: " + workflow.id + " , desc: " + workflow.description + " , start date: " + workflow.startDate + " , def: " + workflow.definition.title); + } + } + + else if (command[1].equals("paths")) + { + String workflowId = (command.length == 3) ? command[2] : (currentPath == null) ? null : currentPath.instance.id; + if (workflowId == null) + { + return "Syntax Error. Workflow Id not specified.\n"; + } + List paths = workflowService.getWorkflowPaths(workflowId); + for (WorkflowPath path : paths) + { + out.println("path id: " + path.id + " , node: " + path.node.name); + } + } + + else if (command[1].equals("tasks")) + { + String pathId = (command.length == 3) ? command[2] : (currentPath == null) ? null : currentPath.id; + if (pathId == null) + { + return "Syntax Error. Path Id not specified.\n"; + } + List tasks = workflowService.getTasksForWorkflowPath(pathId); + for (WorkflowTask task : tasks) + { + out.println("task id: " + task.id + " , name: " + task.name + " , properties: " + task.properties.size()); + } + } + + else if (command[1].equals("transitions")) + { + String workflowId = (command.length == 3) ? command[2] : (currentPath == null) ? null : currentPath.instance.id; + if (workflowId == null) + { + return "Syntax Error. Workflow Id not specified.\n"; + } + List paths = workflowService.getWorkflowPaths(workflowId); + for (WorkflowPath path : paths) + { + out.println("path: " + path.id + " , node: " + path.node.name + " , active: " + path.active); + List tasks = workflowService.getTasksForWorkflowPath(path.id); + for (WorkflowTask task : tasks) + { + out.println(" task id: " + task.id + " , name: " + task.name + " , properties: " + task.properties.size()); + } + for (WorkflowTransition transition : path.node.transitions) + { + out.println(" transition id: " + ((transition.id == null || transition.id.equals("")) ? "[default]" : transition.id) + " , title: " + transition.title); + } + } + } + + else if (command[1].equals("my")) + { + if (command.length != 3) + { + return "Syntax Error.\n"; + } + + if (command[2].equals("tasks")) + { + out.println(AuthenticationUtil.getCurrentUserName() + ":"); + List tasks = workflowService.getAssignedTasks(AuthenticationUtil.getCurrentUserName(), WorkflowTaskState.IN_PROGRESS); + for (WorkflowTask task : tasks) + { + out.println("id: " + task.id + " , name: " + task.name + " , properties: " + task.properties.size() + " , workflow: " + task.path.instance.id + " , path: " + task.path.id); + } + } + + else if (command[2].equals("completed")) + { + out.println(AuthenticationUtil.getCurrentUserName() + ":"); + List tasks = workflowService.getAssignedTasks(AuthenticationUtil.getCurrentUserName(), WorkflowTaskState.COMPLETED); + for (WorkflowTask task : tasks) + { + out.println("id: " + task.id + " , name " + task.name + " , properties: " + task.properties.size() + " , workflow: " + task.path.instance.id + " , path: " + task.path.id); + } + } + else + { + return "Syntax Error.\n"; + } + } + else + { + return "Syntax Error.\n"; + } + } + + else if (command[0].equals("desc")) + { + if (command.length < 2) + { + return "Syntax Error.\n"; + } + + if (command[1].equals("task")) + { + if (command.length != 3) + { + return "Syntax Error.\n"; + } + WorkflowTask task = workflowService.getTaskById(command[2]); + out.println("id: " + task.id); + out.println("name: " + task.name); + out.println("title: " + task.title); + out.println("description: " + task.description); + out.println("state: " + task.state); + out.println("path: " + task.path.id); + out.println("transitions: " + task.path.node.transitions.length); + for (WorkflowTransition transition : task.path.node.transitions) + { + out.println(" transition: " + ((transition == null || transition.id.equals("")) ? "[default]" : transition.id) + " , title: " + transition.title + " , desc: " + transition.description); + } + out.println("properties: " + task.properties.size()); + for (Map.Entry prop : task.properties.entrySet()) + { + out.println(" " + prop.getKey() + " = " + prop.getValue()); + } + } + + else if (command[1].equals("workflow")) + { + if (command.length != 3) + { + return "Syntax Error.\n"; + } + WorkflowInstance workflow = workflowService.getWorkflowById(command[2]); + out.println("definition: " + workflow.definition.name); + out.println("id: " + workflow.id); + out.println("description: " + workflow.description); + out.println("active: " + workflow.active); + out.println("start date: " + workflow.startDate); + out.println("end date: " + workflow.endDate); + out.println("initiator: " + workflow.initiator); + out.println("context: " + workflow.context); + out.println("package: " + workflow.workflowPackage); + } + else + { + return "Syntax Error.\n"; + } + } + + else if (command[0].equals("deploy")) + { + if (command.length != 2) + { + return "Syntax Error.\n"; + } + ClassPathResource workflowDef = new ClassPathResource(command[1]); + WorkflowDeployment deployment = workflowService.deployDefinition("jbpm", workflowDef.getInputStream(), MimetypeMap.MIMETYPE_XML); + WorkflowDefinition def = deployment.definition; + for (String problem : deployment.problems) + { + out.println(problem); + } + out.println("deployed definition id: " + def.id + " , name: " + def.name + " , title: " + def.title + " , version: " + def.version); + currentDeploy = command[1]; + out.print(interpretCommand("use " + def.id)); + } + + else if (command[0].equals("redeploy")) + { + if (currentDeploy == null) + { + return "nothing to redeploy\n"; + } + out.print(interpretCommand("deploy " + currentDeploy)); + } + + else if (command[0].equals("use")) + { + if (command.length == 1) + { + out.println("definition: " + ((currentWorkflowDef == null) ? "None" : currentWorkflowDef.id + " , name: " + currentWorkflowDef.title)); + out.println("workflow: " + ((currentPath == null) ? "None" : currentPath.instance.id + " , active: " + currentPath.instance.active)); + out.println("path: " + ((currentPath == null) ? "None" : currentPath.id + " , node: " + currentPath.node.title)); + } + else if (command.length > 1) + { + if (command[1].equals("definition")) + { + if (command.length != 3) + { + return "Syntax Error.\n"; + } + WorkflowDefinition def = workflowService.getDefinitionById(command[2]); + if (def == null) + { + return "Not found.\n"; + } + currentWorkflowDef = def; + currentPath = null; + out.print(interpretCommand("use")); + } + + else if (command[1].equals("workflow")) + { + if (command.length != 3) + { + return "Syntax Error.\n"; + } + WorkflowInstance instance = workflowService.getWorkflowById(command[2]); + currentWorkflowDef = instance.definition; + currentPath = workflowService.getWorkflowPaths(instance.id).get(0); + out.print(interpretCommand("use")); + } + else + { + return "Syntax Error.\n"; + } + } + + } + + else if (command[0].equals("user")) + { + if (command.length == 2) + { + AuthenticationUtil.setCurrentUser(command[1]); + } + out.println("using user " + AuthenticationUtil.getCurrentUserName()); + } + + else if (command[0].equals("start")) + { + Map params = new HashMap(); + for (int i = 1; i < command.length; i++) + { + String[] param = command[i].split("="); + QName qname = QName.createQName(param[0], namespaceService); + if (param.length == 1) + { + if (!vars.containsKey(qname)) + { + return "var " + qname + " not found.\n"; + } + params.put(qname, vars.get(qname)); + } + else if (param.length == 2) + { + params.put(qname, param[1]); + } + else + { + return "Syntax Error.\n"; + } + } + WorkflowPath path = workflowService.startWorkflow(currentWorkflowDef.id, params); + out.println("started workflow id: " + path.instance.id + ", path: " + path.id + " , node: " + path.node.name + " , def: " + path.instance.definition.title); + currentPath = path; + } + + else if (command[0].equals("update")) + { + if (command.length < 3) + { + return "Syntax Error.\n"; + } + + if (command[1].equals("task")) + { + if (command.length < 4) + { + return "Syntax Error.\n"; + } + Map params = new HashMap(); + for (int i = 3; i < command.length; i++) + { + String[] param = command[i].split("="); + QName qname = QName.createQName(param[0], namespaceService); + if (param.length == 1) + { + if (!vars.containsKey(qname)) + { + return "var " + qname + " not found.\n"; + } + params.put(qname, vars.get(qname)); + } + else if (param.length == 2) + { + params.put(qname, param[1]); + } + else + { + return "Syntax Error.\n"; + } + } + WorkflowTask task = workflowService.updateTask(command[2], params, null, null); + out.println("updated task id: " + command[2] + ", properties: " + task.properties.size()); + } + else + { + return "Syntax Error.\n"; + } + } + + else if (command[0].equals("signal")) + { + if (command.length < 2) + { + return "Syntax Error.\n"; + } + WorkflowPath path = workflowService.signal(command[1], (command.length == 3) ? command[2] : null); + out.println("signal sent - path id: " + path.id + " , node: " + path.node.name); + } + + else if (command[0].equals("end")) + { + if (command.length < 3) + { + return "Syntax Error.\n"; + } + if (command[1].equals("task")) + { + WorkflowTask task = workflowService.endTask(command[2], (command.length == 4) ? command[3] : null); + out.println("signal sent - path id: " + task.path.id + " , node: " + task.path.node.name); + } + else if (command[1].equals("workflow")) + { + String workflowId = (command.length == 3) ? command[2] : (currentPath == null) ? null : currentPath.instance.id; + if (workflowId == null) + { + return "Syntax Error. Workflow Id not specified.\n"; + } + workflowService.cancelWorkflow(workflowId); + out.println("cancelled workflow" + workflowId); + } + else + { + return "Syntax Error.\n"; + } + } + + else if (command[0].equals("var")) + { + if (command.length == 1) + { + for (Map.Entry entry : vars.entrySet()) + { + out.println(entry.getKey() + " = " + entry.getValue()); + } + } + else if (command.length == 2) + { + String[] param = command[1].split("="); + if (param.length == 0) + { + return "Syntax Error.\n"; + } + if (param.length == 1) + { + QName qname = QName.createQName(param[0], namespaceService); + vars.remove(qname); + out.println("deleted var " + qname); + } + else if (param.length == 2) + { + boolean multi = false; + if (param[0].endsWith("*")) + { + param[0] = param[0].substring(0, param[0].length() -1); + multi = true; + } + QName qname = QName.createQName(param[0], namespaceService); + String[] strValues = param[1].split(","); + if (!multi && strValues.length > 1) + { + return "Syntax Error.\n"; + } + if (!multi) + { + vars.put(qname, strValues[0]); + } + else + { + List values = new ArrayList(); + for (String strValue : strValues) + { + values.add(strValue); + } + vars.put(qname, (Serializable)values); + } + out.println("set var " + qname + " = " + vars.get(qname)); + } + else + { + return "Syntax Error.\n"; + } + } + else if (command.length == 4) + { + if (command[2].equals("person")) + { + boolean multi = false; + if (command[1].endsWith("*")) + { + command[1] = command[1].substring(0, command[1].length() -1); + multi = true; + } + QName qname = QName.createQName(command[1], namespaceService); + String[] strValues = command[3].split(","); + if (!multi && strValues.length > 1) + { + return "Syntax Error.\n"; + } + if (!multi) + { + NodeRef auth = personService.getPerson(strValues[0]); + vars.put(qname, auth); + } + else + { + List values = new ArrayList(); + for (String strValue : strValues) + { + NodeRef auth = personService.getPerson(strValue); + values.add(auth); + } + vars.put(qname, (Serializable)values); + } + out.println("set var " + qname + " = " + vars.get(qname)); + } + else + { + return "Syntax Error.\n"; + } + } + else + { + return "Syntax Error.\n"; + } + } + + else + { + return "Syntax Error.\n"; + } + + out.flush(); + String retVal = new String(bout.toByteArray()); + out.close(); + return retVal; + } + + + /** + * Get currently used workflow definition + * + * @return workflow definition + */ + public WorkflowDefinition getCurrentWorkflowDef() + { + return currentWorkflowDef; + } + + /** + * Get current user name + * + * @return user name + */ + public String getCurrentUserName() + { + return AuthenticationUtil.getCurrentUserName(); + } + +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/workflow/WorkflowPackageImpl.java b/source/java/org/alfresco/repo/workflow/WorkflowPackageImpl.java index dc8737b20d..f0dca684f4 100644 --- a/source/java/org/alfresco/repo/workflow/WorkflowPackageImpl.java +++ b/source/java/org/alfresco/repo/workflow/WorkflowPackageImpl.java @@ -25,6 +25,7 @@ import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.cmr.workflow.WorkflowException; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; @@ -47,6 +48,7 @@ public class WorkflowPackageImpl implements WorkflowPackageComponent private SearchService searchService; private NodeService nodeService; private NamespaceService namespaceService; + private PermissionService permissionService; private NodeRef systemWorkflowContainer = null; @@ -74,6 +76,11 @@ public class WorkflowPackageImpl implements WorkflowPackageComponent this.nodeService = nodeService; } + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } + /** * @param namespaceService namespace service */ @@ -113,6 +120,8 @@ public class WorkflowPackageImpl implements WorkflowPackageComponent QName qname = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, containerName); ChildAssociationRef childRef = nodeService.createNode(packages, ContentModel.ASSOC_CONTAINS, qname, ContentModel.TYPE_SYSTEM_FOLDER); container = childRef.getChildRef(); + // TODO: For now, grant full access to everyone + permissionService.setPermission(container, PermissionService.ALL_AUTHORITIES, PermissionService.ALL_PERMISSIONS, true); isSystemPackage = true; } diff --git a/source/java/org/alfresco/repo/workflow/jbpm/JBPMEngine.java b/source/java/org/alfresco/repo/workflow/jbpm/JBPMEngine.java index 9e1cfbabf0..74a293728f 100644 --- a/source/java/org/alfresco/repo/workflow/jbpm/JBPMEngine.java +++ b/source/java/org/alfresco/repo/workflow/jbpm/JBPMEngine.java @@ -311,10 +311,25 @@ public class JBPMEngine extends BPMEngine /* (non-Javadoc) * @see org.alfresco.repo.workflow.WorkflowDefinitionComponent#getDefinitionById(java.lang.String) */ - public WorkflowDefinition getDefinitionById(String workflowDefinitionId) + public WorkflowDefinition getDefinitionById(final String workflowDefinitionId) { - // TODO - throw new UnsupportedOperationException(); + try + { + return (WorkflowDefinition)jbpmTemplate.execute(new JbpmCallback() + { + public Object doInJbpm(JbpmContext context) + { + // retrieve process + GraphSession graphSession = context.getGraphSession(); + ProcessDefinition processDefinition = graphSession.getProcessDefinition(getJbpmId(workflowDefinitionId)); + return processDefinition == null ? null : createWorkflowDefinition(processDefinition); + } + }); + } + catch(JbpmException e) + { + throw new WorkflowException("Failed to retrieve workflow definition '" + workflowDefinitionId + "'", e); + } } /* (non-Javadoc) @@ -1714,7 +1729,7 @@ public class JBPMEngine extends BPMEngine workflowTransition.id = transition.getName(); Node node = transition.getFrom(); workflowTransition.isDefault = node.getDefaultLeavingTransition().equals(transition); - if (workflowTransition.id.length() == 0) + if (workflowTransition.id == null || workflowTransition.id.length() == 0) { workflowTransition.title = getLabel(DEFAULT_TRANSITION_LABEL, TITLE_LABEL, workflowTransition.id); workflowTransition.description = getLabel(DEFAULT_TRANSITION_LABEL, DESC_LABEL, workflowTransition.title); diff --git a/source/java/org/alfresco/repo/workflow/jbpm/JBPMEngineTest.java b/source/java/org/alfresco/repo/workflow/jbpm/JBPMEngineTest.java index 3e927d4bba..47c9465e0d 100644 --- a/source/java/org/alfresco/repo/workflow/jbpm/JBPMEngineTest.java +++ b/source/java/org/alfresco/repo/workflow/jbpm/JBPMEngineTest.java @@ -341,8 +341,10 @@ public class JBPMEngineTest extends BaseSpringTest public void testSignal() { + Map parameters = new HashMap(); + parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "testNode"), testNodeRef); WorkflowDefinition workflowDef = getTestDefinition(); - WorkflowPath path = workflowComponent.startWorkflow(workflowDef.id, null); + WorkflowPath path = workflowComponent.startWorkflow(workflowDef.id, parameters); assertNotNull(path); WorkflowPath updatedPath = workflowComponent.signal(path.id, path.node.transitions[1].id); assertNotNull(updatedPath); @@ -374,6 +376,26 @@ public class JBPMEngineTest extends BaseSpringTest } + public void xtestMultiAssign() + { + WorkflowDefinition workflowDef = getTestDefinition(); + List bpm_assignees = new ArrayList(); + bpm_assignees.add("admin"); + bpm_assignees.add("bob"); + bpm_assignees.add("fred"); + Map parameters = new HashMap(); + parameters.put(QName.createQName(NamespaceService.BPM_MODEL_1_0_URI, "assignees"), (Serializable)bpm_assignees); + parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "testNode"), testNodeRef); + WorkflowPath path = workflowComponent.startWorkflow(workflowDef.id, parameters); + assertNotNull(path); + List tasks = workflowComponent.getTasksForWorkflowPath(path.id); + assertNotNull(tasks); + assertEquals(1, tasks.size()); + WorkflowTask updatedTask = taskComponent.endTask(tasks.get(0).id, "multi"); + assertNotNull(updatedTask); + } + + public void testEndTask() { WorkflowDefinition workflowDef = getTestDefinition(); diff --git a/source/java/org/alfresco/service/cmr/repository/CopyService.java b/source/java/org/alfresco/service/cmr/repository/CopyService.java index d69e77e82a..857fb7431a 100644 --- a/source/java/org/alfresco/service/cmr/repository/CopyService.java +++ b/source/java/org/alfresco/service/cmr/repository/CopyService.java @@ -74,6 +74,7 @@ public interface CopyService * @param destinationAssocTypeQName the type of the new child assoc * @param destinationQName the qualified name of the child association from the * parent to the new node + * @param copyChildren indicates that the children of the node should also be copied * * @return the new node reference */ @@ -85,10 +86,32 @@ public interface CopyService QName destinationQName, boolean copyChildren); + /** + * @see CopyService#copy(NodeRef, NodeRef, QName, QName, boolean) + * + * Ensures the copy name is the same as the origional or is renamed to prevent duplicate names. + * + * @param sourceNodeRef the node reference used as the source of the copy + * @param destinationParent the intended parent of the new node + * @param destinationAssocTypeQName the type of the new child assoc + * @param destinationQName the qualified name of the child association from the + * parent to the new node + * @param copyChildren indicates that the children of the node should also be copied + * + * @return the new node reference + */ + @Auditable(key = Auditable.Key.ARG_0, parameters = {"sourceNodeRef", "destinationParent", "destinationAssocTypeQName", "destinationQName", "copyChildren"}) + public NodeRef copyAndRename( + NodeRef sourceNodeRef, + NodeRef destinationParent, + QName destinationAssocTypeQName, + QName destinationQName, + boolean copyChildren); + /** * By default children of the source node are not copied. * - * @see NodeCopyService#copy(NodeRef, NodeRef, QName, QName, boolean) + * @see CopyService#copy(NodeRef, NodeRef, QName, QName, boolean) * * @param sourceNodeRef the node reference used as the source of the copy * @param destinationParent the intended parent of the new node @@ -142,4 +165,5 @@ public interface CopyService */ @Auditable(key = Auditable.Key.ARG_0, parameters = {"nodeRef"}) public List getCopies(NodeRef nodeRef); + } diff --git a/source/java/org/alfresco/service/cmr/repository/TemplateNode.java b/source/java/org/alfresco/service/cmr/repository/TemplateNode.java index 6d2a05b55c..bd3ce6bea0 100644 --- a/source/java/org/alfresco/service/cmr/repository/TemplateNode.java +++ b/source/java/org/alfresco/service/cmr/repository/TemplateNode.java @@ -60,7 +60,7 @@ import freemarker.ext.dom.NodeModel; * * @author Kevin Roast */ -public final class TemplateNode implements Serializable +public class TemplateNode implements Serializable { private static final long serialVersionUID = 1234390333739034171L; @@ -78,7 +78,7 @@ public final class TemplateNode implements Serializable private Map> assocs = null; /** Cached values */ - private NodeRef nodeRef; + protected NodeRef nodeRef; private String name; private QName type; private String path; @@ -87,15 +87,16 @@ public final class TemplateNode implements Serializable private QNameMap properties; private List permissions = null; private boolean propsRetrieved = false; - private ServiceRegistry services = null; + protected ServiceRegistry services = null; private Boolean isDocument = null; private Boolean isContainer = null; private String displayPath = null; private String mimetype = null; private Long size = null; - private TemplateImageResolver imageResolver = null; + protected TemplateImageResolver imageResolver = null; private TemplateNode parent = null; private ChildAssociationRef primaryParentAssoc = null; + private Boolean isCategory = null; // ------------------------------------------------------------------------------ @@ -360,6 +361,20 @@ public final class TemplateNode implements Serializable return locked; } + /** + * @return true if the node is a Category instance + */ + public boolean getIsCategory() + { + if (isCategory == null) + { + DictionaryService dd = this.services.getDictionaryService(); + isCategory = Boolean.valueOf(dd.isSubClass(getType(), ContentModel.TYPE_CATEGORY)); + } + + return isCategory.booleanValue(); + } + /** * @return the parent node */ @@ -482,17 +497,17 @@ public final class TemplateNode implements Serializable */ public NodeModel getXmlNodeModel() { - try - { - return NodeModel.parse(new InputSource(new StringReader(getContent()))); - } - catch (Throwable err) - { - if (logger.isDebugEnabled()) - logger.debug(err.getMessage(), err); - - return null; - } + try + { + return NodeModel.parse(new InputSource(new StringReader(getContent()))); + } + catch (Throwable err) + { + if (logger.isDebugEnabled()) + logger.debug(err.getMessage(), err); + + return null; + } } /** @@ -661,14 +676,18 @@ public final class TemplateNode implements Serializable } + // ------------------------------------------------------------------------------ // Audit API - + /** + * @return a list of AuditInfo objects describing the Audit Trail for this node instance + */ public List getAuditTrail() { return this.services.getAuditService().getAuditTrail(this.nodeRef); } + // ------------------------------------------------------------------------------ // Misc helpers