diff --git a/config/alfresco/attributes-service-context.xml b/config/alfresco/attributes-service-context.xml
index 45eac09fe8..a8b8bb0fe9 100644
--- a/config/alfresco/attributes-service-context.xml
+++ b/config/alfresco/attributes-service-context.xml
@@ -10,4 +10,30 @@
+
+
+
+
+ org.alfresco.repo.attributes.PropTablesCleanupJob
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${attributes.propcleaner.cronExpression}
+
+
\ No newline at end of file
diff --git a/config/alfresco/dao/dao-context.xml b/config/alfresco/dao/dao-context.xml
index ba566a5ddd..7d3a7b50ec 100644
--- a/config/alfresco/dao/dao-context.xml
+++ b/config/alfresco/dao/dao-context.xml
@@ -175,6 +175,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -193,6 +212,8 @@
+
+
diff --git a/config/alfresco/dbscripts/utility/org.hibernate.dialect.MySQLInnoDBDialect/CleanAlfPropTables.sql b/config/alfresco/dbscripts/utility/org.hibernate.dialect.MySQLInnoDBDialect/CleanAlfPropTables.sql
new file mode 100644
index 0000000000..c0b3845656
--- /dev/null
+++ b/config/alfresco/dbscripts/utility/org.hibernate.dialect.MySQLInnoDBDialect/CleanAlfPropTables.sql
@@ -0,0 +1,64 @@
+--BEGIN TXN
+
+-- get all active references to alf_prop_root
+--FOREACH alf_audit_app.id system.upgrade.clean_alf_prop_tables.batchsize
+create table temp_prop_root_ref as select disabled_paths_id as id from alf_audit_app where id >= ${LOWERBOUND} and id <= ${UPPERBOUND};
+create index idx_temp_prop_root_ref_id on temp_prop_root_ref(id);
+--FOREACH alf_audit_entry.audit_values_id system.upgrade.clean_alf_prop_tables.batchsize
+insert into temp_prop_root_ref select audit_values_id from alf_audit_entry where audit_values_id >= ${LOWERBOUND} and audit_values_id <= ${UPPERBOUND};
+--FOREACH alf_prop_unique_ctx.prop1_id system.upgrade.clean_alf_prop_tables.batchsize
+insert into temp_prop_root_ref select prop1_id from alf_prop_unique_ctx where prop1_id is not null and prop1_id >= ${LOWERBOUND} and prop1_id <= ${UPPERBOUND};
+
+-- determine the obsolete entries from alf_prop_root
+--FOREACH alf_prop_root.id system.upgrade.clean_alf_prop_tables.batchsize
+create table temp_prop_root_abs as select alf_prop_root.id from alf_prop_root left join temp_prop_root_ref on temp_prop_root_ref.id = alf_prop_root.id where temp_prop_root_ref.id is null and alf_prop_root.id >= ${LOWERBOUND} and alf_prop_root.id <= ${UPPERBOUND};
+create index idx_temp_prop_root_abs_id on temp_prop_root_abs(id);
+
+-- clear alf_prop_root which cascades DELETE to alf_prop_link
+--FOREACH temp_prop_root_abs.id system.upgrade.clean_alf_prop_tables.batchsize
+delete from alf_prop_root where id in (select id from temp_prop_root_abs where id >= ${LOWERBOUND} and id <= ${UPPERBOUND});
+
+-- get all active references to alf_prop_value
+
+--FOREACH alf_prop_value.id system.upgrade.clean_alf_prop_tables.batchsize
+create table temp_prop_val_ref as select id from alf_prop_value where id in (select app_name_id from alf_audit_app) and id >= ${LOWERBOUND} and id <= ${UPPERBOUND};
+create index idx_temp_prop_val_ref_id on temp_prop_val_ref(id);
+--FOREACH alf_audit_entry.audit_user_id system.upgrade.clean_alf_prop_tables.batchsize
+insert into temp_prop_val_ref select audit_user_id from alf_audit_entry where audit_user_id >= ${LOWERBOUND} and audit_user_id <= ${UPPERBOUND};
+--FOREACH alf_prop_link.key_prop_id system.upgrade.clean_alf_prop_tables.batchsize
+insert into temp_prop_val_ref select key_prop_id from alf_prop_link where key_prop_id >= ${LOWERBOUND} and key_prop_id <= ${UPPERBOUND};
+--FOREACH alf_prop_link.value_prop_id system.upgrade.clean_alf_prop_tables.batchsize
+insert into temp_prop_val_ref select value_prop_id from alf_prop_link where value_prop_id >= ${LOWERBOUND} and value_prop_id <= ${UPPERBOUND};
+--FOREACH alf_prop_unique_ctx.value1_prop_id system.upgrade.clean_alf_prop_tables.batchsize
+insert into temp_prop_val_ref select value1_prop_id from alf_prop_unique_ctx where value1_prop_id >= ${LOWERBOUND} and value1_prop_id <= ${UPPERBOUND};
+--FOREACH alf_prop_unique_ctx.value2_prop_id system.upgrade.clean_alf_prop_tables.batchsize
+insert into temp_prop_val_ref select value2_prop_id from alf_prop_unique_ctx where value2_prop_id >= ${LOWERBOUND} and value2_prop_id <= ${UPPERBOUND};
+--FOREACH alf_prop_unique_ctx.value3_prop_id system.upgrade.clean_alf_prop_tables.batchsize
+insert into temp_prop_val_ref select value3_prop_id from alf_prop_unique_ctx where value3_prop_id >= ${LOWERBOUND} and value3_prop_id <= ${UPPERBOUND};
+
+-- determine the obsolete entries from alf_prop_value
+--FOREACH alf_prop_value.id system.upgrade.clean_alf_prop_tables.batchsize
+create table temp_prop_val_abs as select apv.id, apv.persisted_type, apv.long_value from alf_prop_value apv left join temp_prop_val_ref on (apv.id = temp_prop_val_ref.id) where temp_prop_val_ref.id is null and apv.id >= ${LOWERBOUND} and apv.id <= ${UPPERBOUND};
+create index idx_temp_prop_val_abs_id on temp_prop_val_abs(id);
+create index idx_temp_prop_val_abs_per on temp_prop_val_abs(persisted_type, id, long_value);
+
+-- clear the obsolete entries
+--FOREACH temp_prop_val_abs.id system.upgrade.clean_alf_prop_tables.batchsize
+delete from alf_prop_value where id in (select id from temp_prop_val_abs where id >= ${LOWERBOUND} and id <= ${UPPERBOUND});
+
+-- find and clear obsoleted string values
+create table temp_del_str as select temp_prop_val_abs.long_value as string_id from temp_prop_val_abs left join alf_prop_value apv on (apv.id = temp_prop_val_abs.id) where temp_prop_val_abs.persisted_type in (3,5,6) and apv.id is null;
+--FOREACH temp_del_str.string_id system.upgrade.clean_alf_prop_tables.batchsize
+delete from alf_prop_string_value where id in (select id from temp_del_str where id >= ${LOWERBOUND} and id <= ${UPPERBOUND});
+
+-- find and clear obsoleted serialized values
+create table temp_del_ser as select temp_prop_val_abs.long_value as string_id from temp_prop_val_abs left join alf_prop_value apv on (apv.id = temp_prop_val_abs.id) where temp_prop_val_abs.persisted_type = 4 and apv.id is null;
+--FOREACH temp_del_ser.string_id system.upgrade.clean_alf_prop_tables.batchsize
+delete from alf_prop_serializable_value where id in (select id from temp_del_ser where id >= ${LOWERBOUND} and id <= ${UPPERBOUND});
+
+-- find and clear obsoleted double values
+create table temp_del_double as select temp_prop_val_abs.long_value as string_id from temp_prop_val_abs left join alf_prop_value apv on (apv.id = temp_prop_val_abs.id) where temp_prop_val_abs.persisted_type = 2 and apv.id is null;
+--FOREACH temp_del_double.string_id system.upgrade.clean_alf_prop_tables.batchsize
+delete from alf_prop_double_value where id in (select id from temp_del_double where id >= ${LOWERBOUND} and id <= ${UPPERBOUND});
+
+--END TXN
\ No newline at end of file
diff --git a/config/alfresco/dbscripts/utility/org.hibernate.dialect.MySQLInnoDBDialect/CleanAlfPropTablesPostExec.sql b/config/alfresco/dbscripts/utility/org.hibernate.dialect.MySQLInnoDBDialect/CleanAlfPropTablesPostExec.sql
new file mode 100644
index 0000000000..b8bf57f498
--- /dev/null
+++ b/config/alfresco/dbscripts/utility/org.hibernate.dialect.MySQLInnoDBDialect/CleanAlfPropTablesPostExec.sql
@@ -0,0 +1,12 @@
+--BEGIN TXN
+
+-- cleanup temporary structures
+drop table temp_prop_root_ref; --(optional)
+drop table temp_prop_root_abs; --(optional)
+drop table temp_prop_val_ref; --(optional)
+drop table temp_prop_val_abs; --(optional)
+drop table temp_del_str; --(optional)
+drop table temp_del_ser; --(optional)
+drop table temp_del_double; --(optional)
+
+--END TXN
\ No newline at end of file
diff --git a/config/alfresco/dbscripts/utility/org.hibernate.dialect.PostgreSQLDialect/CleanAlfPropTables.sql b/config/alfresco/dbscripts/utility/org.hibernate.dialect.PostgreSQLDialect/CleanAlfPropTables.sql
new file mode 100644
index 0000000000..08eaab2f7b
--- /dev/null
+++ b/config/alfresco/dbscripts/utility/org.hibernate.dialect.PostgreSQLDialect/CleanAlfPropTables.sql
@@ -0,0 +1,63 @@
+--BEGIN TXN
+
+-- get all active references to alf_prop_root
+--FOREACH alf_audit_app.id system.upgrade.clean_alf_prop_tables.batchsize
+create temp table temp_prop_root_ref as select disabled_paths_id as id from alf_audit_app where id >= ${LOWERBOUND} and id <= ${UPPERBOUND};
+create index idx_temp_prop_root_ref_id on temp_prop_root_ref(id);
+--FOREACH alf_audit_entry.audit_values_id system.upgrade.clean_alf_prop_tables.batchsize
+insert into temp_prop_root_ref select audit_values_id from alf_audit_entry where audit_values_id >= ${LOWERBOUND} and audit_values_id <= ${UPPERBOUND};
+--FOREACH alf_prop_unique_ctx.prop1_id system.upgrade.clean_alf_prop_tables.batchsize
+insert into temp_prop_root_ref select prop1_id from alf_prop_unique_ctx where prop1_id is not null and prop1_id >= ${LOWERBOUND} and prop1_id <= ${UPPERBOUND};
+
+-- determine the obsolete entries from alf_prop_root
+--FOREACH alf_prop_root.id system.upgrade.clean_alf_prop_tables.batchsize
+create temp table temp_prop_root_abs as select alf_prop_root.id from alf_prop_root left join temp_prop_root_ref on temp_prop_root_ref.id = alf_prop_root.id where temp_prop_root_ref.id is null and alf_prop_root.id >= ${LOWERBOUND} and alf_prop_root.id <= ${UPPERBOUND};
+create index idx_temp_prop_root_abs_id on temp_prop_root_abs(id);
+
+-- clear alf_prop_root which cascades DELETE to alf_prop_link
+--FOREACH temp_prop_root_abs.id system.upgrade.clean_alf_prop_tables.batchsize
+delete from alf_prop_root where id in (select id from temp_prop_root_abs where id >= ${LOWERBOUND} and id <= ${UPPERBOUND});
+
+-- get all active references to alf_prop_value
+--FOREACH alf_prop_value.id system.upgrade.clean_alf_prop_tables.batchsize
+create temp table temp_prop_val_ref as select id from alf_prop_value where id in (select app_name_id from alf_audit_app) and id >= ${LOWERBOUND} and id <= ${UPPERBOUND};
+create index idx_temp_prop_val_ref_id on temp_prop_val_ref(id);
+--FOREACH alf_audit_entry.audit_user_id system.upgrade.clean_alf_prop_tables.batchsize
+insert into temp_prop_val_ref select audit_user_id from alf_audit_entry where audit_user_id >= ${LOWERBOUND} and audit_user_id <= ${UPPERBOUND};
+--FOREACH alf_prop_link.key_prop_id system.upgrade.clean_alf_prop_tables.batchsize
+insert into temp_prop_val_ref select key_prop_id from alf_prop_link where key_prop_id >= ${LOWERBOUND} and key_prop_id <= ${UPPERBOUND};
+--FOREACH alf_prop_link.value_prop_id system.upgrade.clean_alf_prop_tables.batchsize
+insert into temp_prop_val_ref select value_prop_id from alf_prop_link where value_prop_id >= ${LOWERBOUND} and value_prop_id <= ${UPPERBOUND};
+--FOREACH alf_prop_unique_ctx.value1_prop_id system.upgrade.clean_alf_prop_tables.batchsize
+insert into temp_prop_val_ref select value1_prop_id from alf_prop_unique_ctx where value1_prop_id >= ${LOWERBOUND} and value1_prop_id <= ${UPPERBOUND};
+--FOREACH alf_prop_unique_ctx.value2_prop_id system.upgrade.clean_alf_prop_tables.batchsize
+insert into temp_prop_val_ref select value2_prop_id from alf_prop_unique_ctx where value2_prop_id >= ${LOWERBOUND} and value2_prop_id <= ${UPPERBOUND};
+--FOREACH alf_prop_unique_ctx.value3_prop_id system.upgrade.clean_alf_prop_tables.batchsize
+insert into temp_prop_val_ref select value3_prop_id from alf_prop_unique_ctx where value3_prop_id >= ${LOWERBOUND} and value3_prop_id <= ${UPPERBOUND};
+
+-- determine the obsolete entries from alf_prop_value
+--FOREACH alf_prop_value.id system.upgrade.clean_alf_prop_tables.batchsize
+create temp table temp_prop_val_abs as select apv.id, apv.persisted_type, apv.long_value from alf_prop_value apv left join temp_prop_val_ref on (apv.id = temp_prop_val_ref.id) where temp_prop_val_ref.id is null and apv.id >= ${LOWERBOUND} and apv.id <= ${UPPERBOUND};
+create index idx_temp_prop_val_abs_id on temp_prop_val_abs(id);
+create index idx_temp_prop_val_abs_per on temp_prop_val_abs(persisted_type, id, long_value);
+
+-- clear the obsolete entries
+--FOREACH temp_prop_val_abs.id system.upgrade.clean_alf_prop_tables.batchsize
+delete from alf_prop_value where id in (select id from temp_prop_val_abs where id >= ${LOWERBOUND} and id <= ${UPPERBOUND});
+
+-- find and clear obsoleted string values
+create table temp_del_str as select temp_prop_val_abs.long_value as string_id from temp_prop_val_abs left join alf_prop_value apv on (apv.id = temp_prop_val_abs.id) where temp_prop_val_abs.persisted_type in (3,5,6) and apv.id is null;
+--FOREACH temp_del_str.string_id system.upgrade.clean_alf_prop_tables.batchsize
+delete from alf_prop_string_value where id in (select id from temp_del_str where id >= ${LOWERBOUND} and id <= ${UPPERBOUND});
+
+-- find and clear obsoleted serialized values
+create table temp_del_ser as select temp_prop_val_abs.long_value as string_id from temp_prop_val_abs left join alf_prop_value apv on (apv.id = temp_prop_val_abs.id) where temp_prop_val_abs.persisted_type = 4 and apv.id is null;
+--FOREACH temp_del_ser.string_id system.upgrade.clean_alf_prop_tables.batchsize
+delete from alf_prop_serializable_value where id in (select id from temp_del_ser where id >= ${LOWERBOUND} and id <= ${UPPERBOUND});
+
+-- find and clear obsoleted double values
+create table temp_del_double as select temp_prop_val_abs.long_value as string_id from temp_prop_val_abs left join alf_prop_value apv on (apv.id = temp_prop_val_abs.id) where temp_prop_val_abs.persisted_type = 2 and apv.id is null;
+--FOREACH temp_del_double.string_id system.upgrade.clean_alf_prop_tables.batchsize
+delete from alf_prop_double_value where id in (select id from temp_del_double where id >= ${LOWERBOUND} and id <= ${UPPERBOUND});
+
+--END TXN
diff --git a/config/alfresco/dbscripts/utility/org.hibernate.dialect.PostgreSQLDialect/CleanAlfPropTablesPostExec.sql b/config/alfresco/dbscripts/utility/org.hibernate.dialect.PostgreSQLDialect/CleanAlfPropTablesPostExec.sql
new file mode 100644
index 0000000000..b8bf57f498
--- /dev/null
+++ b/config/alfresco/dbscripts/utility/org.hibernate.dialect.PostgreSQLDialect/CleanAlfPropTablesPostExec.sql
@@ -0,0 +1,12 @@
+--BEGIN TXN
+
+-- cleanup temporary structures
+drop table temp_prop_root_ref; --(optional)
+drop table temp_prop_root_abs; --(optional)
+drop table temp_prop_val_ref; --(optional)
+drop table temp_prop_val_abs; --(optional)
+drop table temp_del_str; --(optional)
+drop table temp_del_ser; --(optional)
+drop table temp_del_double; --(optional)
+
+--END TXN
\ No newline at end of file
diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties
index 26bcdb6e52..86aa36d2dd 100644
--- a/config/alfresco/repository.properties
+++ b/config/alfresco/repository.properties
@@ -1108,4 +1108,8 @@ system.lockTryTimeout.PolicyComponentImpl=${system.lockTryTimeout}
#
system.patch.surfConfigFolder.deferred=false
# Default value. i.e. never run. It can be triggered using JMX
-system.patch.surfConfigFolder.cronExpression=* * * * * ? 2099
\ No newline at end of file
+system.patch.surfConfigFolder.cronExpression=* * * * * ? 2099
+
+# Scheduled job to clean up unused properties from the alf_prop_xxx tables.
+# Default setting is for it never to run.
+attributes.propcleaner.cronExpression=* * * * * ? 2099
diff --git a/source/java/org/alfresco/repo/attributes/PropTablesCleanupJob.java b/source/java/org/alfresco/repo/attributes/PropTablesCleanupJob.java
new file mode 100644
index 0000000000..ebe3323fb7
--- /dev/null
+++ b/source/java/org/alfresco/repo/attributes/PropTablesCleanupJob.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2005-2014 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.repo.attributes;
+
+import org.alfresco.repo.domain.propval.PropertyValueDAO;
+import org.quartz.Job;
+import org.quartz.JobDataMap;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+
+/**
+ * Cleanup job to initiate cleaning of unused values from the alf_prop_xxx tables.
+ *
+ * @author Matt Ward
+ */
+public class PropTablesCleanupJob implements Job
+{
+ protected static final Object PROPERTY_VALUE_DAO_KEY = "propertyValueDAO";
+
+ @Override
+ public void execute(JobExecutionContext jobCtx) throws JobExecutionException
+ {
+ JobDataMap jobData = jobCtx.getJobDetail().getJobDataMap();
+
+ PropertyValueDAO propertyValueDAO = (PropertyValueDAO) jobData.get(PROPERTY_VALUE_DAO_KEY);
+ if (propertyValueDAO == null)
+ {
+ throw new IllegalArgumentException(PROPERTY_VALUE_DAO_KEY + " in job data map was null");
+ }
+
+ propertyValueDAO.cleanupUnusedValues();
+ }
+}
diff --git a/source/java/org/alfresco/repo/domain/propval/AbstractPropertyValueDAOImpl.java b/source/java/org/alfresco/repo/domain/propval/AbstractPropertyValueDAOImpl.java
index 0248fe97e5..13ba78d3f6 100644
--- a/source/java/org/alfresco/repo/domain/propval/AbstractPropertyValueDAOImpl.java
+++ b/source/java/org/alfresco/repo/domain/propval/AbstractPropertyValueDAOImpl.java
@@ -1588,4 +1588,15 @@ public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO
// This will have put the values into the correct containers
return result;
}
+
+ protected void clearCaches()
+ {
+ propertyClassCache.clear();
+ propertyDateValueCache.clear();
+ propertyStringValueCache.clear();
+ propertyDoubleValueCache.clear();
+ propertySerializableValueCache.clear();
+ propertyCache.clear();
+ propertyValueCache.clear();
+ }
}
diff --git a/source/java/org/alfresco/repo/domain/propval/PropertyValueDAO.java b/source/java/org/alfresco/repo/domain/propval/PropertyValueDAO.java
index be3b223aad..6c5df7d40e 100644
--- a/source/java/org/alfresco/repo/domain/propval/PropertyValueDAO.java
+++ b/source/java/org/alfresco/repo/domain/propval/PropertyValueDAO.java
@@ -362,4 +362,9 @@ public interface PropertyValueDAO
* @throws IllegalArgumentException if rows don't all share the same root property ID
*/
Serializable convertPropertyIdSearchRows(List rows);
+
+ /**
+ * Remove orphaned properties.
+ */
+ void cleanupUnusedValues();
}
diff --git a/source/java/org/alfresco/repo/domain/propval/ibatis/PropertyValueDAOImpl.java b/source/java/org/alfresco/repo/domain/propval/ibatis/PropertyValueDAOImpl.java
index d37ab556f9..d6b2b36344 100644
--- a/source/java/org/alfresco/repo/domain/propval/ibatis/PropertyValueDAOImpl.java
+++ b/source/java/org/alfresco/repo/domain/propval/ibatis/PropertyValueDAOImpl.java
@@ -38,6 +38,7 @@ import org.alfresco.repo.domain.propval.PropertyStringValueEntity;
import org.alfresco.repo.domain.propval.PropertyUniqueContextEntity;
import org.alfresco.repo.domain.propval.PropertyValueEntity;
import org.alfresco.repo.domain.propval.PropertyValueEntity.PersistedType;
+import org.alfresco.repo.domain.schema.script.ScriptBundleExecutor;
import org.alfresco.util.Pair;
import org.apache.ibatis.session.ResultContext;
import org.apache.ibatis.session.ResultHandler;
@@ -98,11 +99,18 @@ public class PropertyValueDAOImpl extends AbstractPropertyValueDAOImpl
private SqlSessionTemplate template;
+ private ScriptBundleExecutor scriptExecutor;
+
public final void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate)
{
this.template = sqlSessionTemplate;
}
+ public void setScriptExecutor(ScriptBundleExecutor scriptExecutor)
+ {
+ this.scriptExecutor = scriptExecutor;
+ }
+
//================================
// 'alf_prop_class' accessors
@@ -672,4 +680,31 @@ public class PropertyValueDAOImpl extends AbstractPropertyValueDAOImpl
entity.setId(rootPropId);
return template.delete(DELETE_PROPERTY_LINKS_BY_ROOT_ID, entity);
}
+
+ @Override
+ public void cleanupUnusedValues()
+ {
+ // execute clean up in case of previous failures
+ scriptExecutor.exec("alfresco/dbscripts/utility/${db.script.dialect}", "CleanAlfPropTablesPostExec.sql");
+ try
+ {
+ scriptExecutor.exec("alfresco/dbscripts/utility/${db.script.dialect}", "CleanAlfPropTables.sql");
+ }
+ finally
+ {
+ try
+ {
+ // execute clean up
+ scriptExecutor.exec("alfresco/dbscripts/utility/${db.script.dialect}", "CleanAlfPropTablesPostExec.sql");
+ }
+ catch (Exception e)
+ {
+ if (logger.isErrorEnabled())
+ {
+ logger.error("The cleanup failed with an error: ", e);
+ }
+ }
+ clearCaches();
+ }
+ }
}
diff --git a/source/java/org/alfresco/repo/domain/schema/script/ScriptBundleExecutor.java b/source/java/org/alfresco/repo/domain/schema/script/ScriptBundleExecutor.java
new file mode 100644
index 0000000000..0e4bc1e764
--- /dev/null
+++ b/source/java/org/alfresco/repo/domain/schema/script/ScriptBundleExecutor.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2005-2014 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.repo.domain.schema.script;
+
+/**
+ * Executes a set of zero or more SQL scripts.
+ *
+ * @author Matt Ward
+ */
+public interface ScriptBundleExecutor
+{
+ /**
+ * Runs a bundle of scripts. If any script within the bundle fails, then the rest of the files are not run.
+ *
+ * @param dir Directory where the script bundle may be found.
+ * @param scripts Names of the SQL scripts to run, relative to the specified directory.
+ */
+ void exec(String dir, String... scripts);
+
+ /**
+ * Runs a bundle of scripts. If any script within the bundle fails, then the rest of the files
+ * are not run, with the exception of postScript - which is always run (a clean-up script for example).
+ *
+ * @param dir Directory where the script bundle may be found.
+ * @param postScript A script that is always run after the other scripts.
+ * @param scripts Names of the SQL scripts to run, relative to the specified directory.
+ */
+ void execWithPostScript(String dir, String postScript, String... scripts);
+}
diff --git a/source/java/org/alfresco/repo/domain/schema/script/ScriptBundleExecutorImpl.java b/source/java/org/alfresco/repo/domain/schema/script/ScriptBundleExecutorImpl.java
new file mode 100644
index 0000000000..e58a246568
--- /dev/null
+++ b/source/java/org/alfresco/repo/domain/schema/script/ScriptBundleExecutorImpl.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2005-2014 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.repo.domain.schema.script;
+
+import java.io.File;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * {@link ScriptBundleExecutor} implementation. Uses the supplied {@link ScriptExecutor}
+ * to invoke multiple SQL scripts in a particular directory.
+ *
+ * @author Matt Ward
+ */
+public class ScriptBundleExecutorImpl implements ScriptBundleExecutor
+{
+ private ScriptExecutor scriptExecutor;
+ protected Log log = LogFactory.getLog(ScriptBundleExecutorImpl.class);
+
+ public ScriptBundleExecutorImpl(ScriptExecutor scriptExecutor)
+ {
+ this.scriptExecutor = scriptExecutor;
+ }
+
+ @Override
+ public void exec(String dir, String... scripts)
+ {
+ for (String name : scripts)
+ {
+ File file = new File(dir, name);
+ try
+ {
+ scriptExecutor.executeScriptUrl(file.getPath());
+ }
+ catch (Throwable e)
+ {
+ log.error("Unable to run SQL script: dir=" + dir + ", name="+name, e);
+ // Do not run any more scripts.
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void execWithPostScript(String dir, String postScript, String... scripts)
+ {
+ try
+ {
+ exec(dir, scripts);
+ }
+ finally
+ {
+ // Always run the post-script.
+ exec(dir, postScript);
+ }
+ }
+}
diff --git a/source/java/org/alfresco/repo/domain/schema/script/ScriptExecutor.java b/source/java/org/alfresco/repo/domain/schema/script/ScriptExecutor.java
new file mode 100644
index 0000000000..7bdcbc9742
--- /dev/null
+++ b/source/java/org/alfresco/repo/domain/schema/script/ScriptExecutor.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2005-2014 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.repo.domain.schema.script;
+
+/**
+ * Defines a SQL script executor that executes a single SQL script.
+ *
+ * @author Matt Ward
+ */
+public interface ScriptExecutor
+{
+ void executeScriptUrl(String scriptUrl) throws Exception;
+}
diff --git a/source/java/org/alfresco/repo/domain/schema/script/ScriptExecutorImpl.java b/source/java/org/alfresco/repo/domain/schema/script/ScriptExecutorImpl.java
new file mode 100644
index 0000000000..51fdc49b62
--- /dev/null
+++ b/source/java/org/alfresco/repo/domain/schema/script/ScriptExecutorImpl.java
@@ -0,0 +1,597 @@
+/*
+ * Copyright (C) 2005-2014 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.repo.domain.schema.script;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.sql.DataSource;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.alfresco.repo.content.filestore.FileContentWriter;
+import org.alfresco.service.cmr.repository.ContentWriter;
+import org.alfresco.util.LogUtil;
+import org.alfresco.util.TempFileProvider;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.hibernate.cfg.Configuration;
+import org.hibernate.dialect.Dialect;
+import org.hibernate.dialect.MySQLInnoDBDialect;
+import org.hibernate.dialect.PostgreSQLDialect;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.orm.hibernate3.LocalSessionFactoryBean;
+
+
+public class ScriptExecutorImpl implements ScriptExecutor
+{
+ /** The placeholder for the configured Dialect class name: ${db.script.dialect} */
+ private static final String PLACEHOLDER_DIALECT = "\\$\\{db\\.script\\.dialect\\}";
+ /** The global property containing the default batch size used by --FOREACH */
+ private static final String PROPERTY_DEFAULT_BATCH_SIZE = "system.upgrade.default.batchsize";
+ private static final String MSG_EXECUTING_GENERATED_SCRIPT = "schema.update.msg.executing_generated_script";
+ private static final String MSG_EXECUTING_COPIED_SCRIPT = "schema.update.msg.executing_copied_script";
+ private static final String MSG_EXECUTING_STATEMENT = "schema.update.msg.executing_statement";
+ private static final String MSG_OPTIONAL_STATEMENT_FAILED = "schema.update.msg.optional_statement_failed";
+ private static final String ERR_STATEMENT_FAILED = "schema.update.err.statement_failed";
+ private static final String ERR_SCRIPT_NOT_FOUND = "schema.update.err.script_not_found";
+ private static final String ERR_STATEMENT_INCLUDE_BEFORE_SQL = "schema.update.err.statement_include_before_sql";
+ private static final String ERR_STATEMENT_VAR_ASSIGNMENT_BEFORE_SQL = "schema.update.err.statement_var_assignment_before_sql";
+ private static final String ERR_STATEMENT_VAR_ASSIGNMENT_FORMAT = "schema.update.err.statement_var_assignment_format";
+ private static final String ERR_STATEMENT_TERMINATOR = "schema.update.err.statement_terminator";
+ private static final int DEFAULT_MAX_STRING_LENGTH = 1024;
+ private static volatile int maxStringLength = DEFAULT_MAX_STRING_LENGTH;
+ private Dialect dialect;
+ private ResourcePatternResolver rpr = new PathMatchingResourcePatternResolver(this.getClass().getClassLoader());
+ private static Log logger = LogFactory.getLog(ScriptExecutorImpl.class);
+ private LocalSessionFactoryBean localSessionFactory;
+ private Properties globalProperties;
+ private ThreadLocal executedStatementsThreadLocal = new ThreadLocal();
+ private DataSource dataSource;
+
+
+ /**
+ * @return Returns the maximum number of characters that a string field can be
+ */
+ public static final int getMaxStringLength()
+ {
+ return ScriptExecutorImpl.maxStringLength;
+ }
+
+ /**
+ * Truncates or returns a string that will fit into the string columns in the schema. Text fields can
+ * either cope with arbitrarily long text fields or have the default limit, {@link #DEFAULT_MAX_STRING_LENGTH}.
+ *
+ * @param value the string to check
+ * @return Returns a string that is short enough for {@link ScriptExecutorImpl#getMaxStringLength()}
+ *
+ * @since 3.2
+ */
+ public static final String trimStringForTextFields(String value)
+ {
+ if (value != null && value.length() > maxStringLength)
+ {
+ return value.substring(0, maxStringLength);
+ }
+ else
+ {
+ return value;
+ }
+ }
+
+ /**
+ * Sets the previously auto-detected Hibernate dialect.
+ *
+ * @param dialect
+ * the dialect
+ */
+ public void setDialect(Dialect dialect)
+ {
+ this.dialect = dialect;
+ }
+
+ public ScriptExecutorImpl()
+ {
+ globalProperties = new Properties();
+ }
+
+
+ public void setLocalSessionFactory(LocalSessionFactoryBean localSessionFactory)
+ {
+ this.localSessionFactory = localSessionFactory;
+ }
+
+ public LocalSessionFactoryBean getLocalSessionFactory()
+ {
+ return localSessionFactory;
+ }
+
+ public void setDataSource(DataSource dataSource)
+ {
+ this.dataSource = dataSource;
+ }
+
+ /**
+ * Sets the properties map from which we look up some configuration settings.
+ *
+ * @param globalProperties
+ * the global properties
+ */
+ public void setGlobalProperties(Properties globalProperties)
+ {
+ this.globalProperties = globalProperties;
+ }
+
+
+ @Override
+ public void executeScriptUrl(String scriptUrl) throws Exception
+ {
+ Configuration cfg = localSessionFactory.getConfiguration();
+ Connection connection = dataSource.getConnection();
+ connection.setAutoCommit(true);
+ try
+ {
+ executeScriptUrl(cfg, connection, scriptUrl);
+ }
+ finally
+ {
+ connection.close();
+ }
+ }
+
+ private void executeScriptUrl(Configuration cfg, Connection connection, String scriptUrl) throws Exception
+ {
+ Dialect dialect = Dialect.getDialect(cfg.getProperties());
+ String dialectStr = dialect.getClass().getSimpleName();
+ 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("AlfrescoSchema-" + dialectStr + "-Update-", ".sql");
+ ContentWriter writer = new FileContentWriter(tempFile);
+ writer.putContent(scriptInputStream);
+ }
+ finally
+ {
+ try { scriptInputStream.close(); } catch (Throwable e) {} // usually a duplicate close
+ }
+ // now execute it
+ String dialectScriptUrl = scriptUrl.replaceAll(PLACEHOLDER_DIALECT, dialect.getClass().getName());
+ // Replace the script placeholders
+ executeScriptFile(cfg, connection, tempFile, dialectScriptUrl);
+ }
+
+ /**
+ * Replaces the dialect placeholder in the resource URL and attempts to find a file for
+ * it. If not found, the dialect hierarchy will be walked until a compatible resource is
+ * found. This makes it possible to have resources that are generic to all dialects.
+ *
+ * @return The Resource, otherwise null
+ */
+ private Resource getDialectResource(Class dialectClass, String resourceUrl)
+ {
+ // replace the dialect placeholder
+ String dialectResourceUrl = resolveDialectUrl(dialectClass, resourceUrl);
+ // get a handle on the resource
+ Resource resource = rpr.getResource(dialectResourceUrl);
+ if (!resource.exists())
+ {
+ // it wasn't found. Get the superclass of the dialect and try again
+ Class superClass = dialectClass.getSuperclass();
+ if (Dialect.class.isAssignableFrom(superClass))
+ {
+ // we still have a Dialect - try again
+ return getDialectResource(superClass, resourceUrl);
+ }
+ else
+ {
+ // we have exhausted all options
+ return null;
+ }
+ }
+ else
+ {
+ // we have a handle to it
+ return resource;
+ }
+ }
+
+ /**
+ * Takes resource URL containing the {@link ScriptExecutorImpl#PLACEHOLDER_DIALECT dialect placeholder text}
+ * and substitutes the placeholder with the name of the given dialect's class.
+ *
+ * For example:
+ *