Index: extensions/modeshape-connector-jdbc-metadata/src/test/java/org/modeshape/connector/meta/jdbc/JdbcMetadataRepositoryTest.java =================================================================== --- extensions/modeshape-connector-jdbc-metadata/src/test/java/org/modeshape/connector/meta/jdbc/JdbcMetadataRepositoryTest.java (revision 2666) +++ extensions/modeshape-connector-jdbc-metadata/src/test/java/org/modeshape/connector/meta/jdbc/JdbcMetadataRepositoryTest.java (working copy) @@ -107,7 +107,7 @@ public class JdbcMetadataRepositoryTest { assertThat(workspace, is(notNullValue())); try { - TestEnvironment.executeDdl(this.source.getDataSource(), "create.ddl", this); + TestEnvironment.executeDdl(this.source.getDataSource(), "create.ddl", this); } catch (SQLException se) { } @@ -327,7 +327,11 @@ public class JdbcMetadataRepositoryTest { invalidSchemaName, nameFactory.create(JdbcMetadataRepository.TABLES_SEGMENT_NAME), nameFactory.create(identifier("sales"))); - assertThat(workspace.getNode(invalidSchemaPath), is(nullValue())); + if (ignoresSchemaPatterns()) { + assertThat(workspace.getNode(invalidSchemaPath), is(notNullValue())); + } else { + assertThat(workspace.getNode(invalidSchemaPath), is(nullValue())); + } } @Test @@ -392,7 +396,11 @@ public class JdbcMetadataRepositoryTest { nameFactory.create(JdbcMetadataRepository.TABLES_SEGMENT_NAME), nameFactory.create(identifier("sales")), nameFactory.create(identifier("amount"))); - assertThat(workspace.getNode(invalidSchemaPath), is(nullValue())); + if (ignoresSchemaPatterns()) { + assertThat(workspace.getNode(invalidSchemaPath), is(notNullValue())); + } else { + assertThat(workspace.getNode(invalidSchemaPath), is(nullValue())); + } } @Test @@ -453,6 +461,15 @@ public class JdbcMetadataRepositoryTest { assertThat(workspace.getNode(invalidSchemaPath), is(nullValue())); } + /** + * Not all databases will use the schema pattern when getting table and column metadata. + * + * @return true if the database ignores the schema patterns. + */ + protected boolean ignoresSchemaPatterns() { + return source.getDriverClassName().toLowerCase().contains("mysql"); + } + private Path pathFor( PathNode node ) { if (node == null) { return null; @@ -464,6 +481,7 @@ public class JdbcMetadataRepositoryTest { return pathFactory.create(node.getParent(), node.getName()); } + private Name nameFor( Property property ) { if (property == null) { return null; Index: extensions/modeshape-connector-jdbc-metadata/src/test/java/org/modeshape/connector/meta/jdbc/TestEnvironment.java =================================================================== --- extensions/modeshape-connector-jdbc-metadata/src/test/java/org/modeshape/connector/meta/jdbc/TestEnvironment.java (revision 2666) +++ extensions/modeshape-connector-jdbc-metadata/src/test/java/org/modeshape/connector/meta/jdbc/TestEnvironment.java (working copy) @@ -23,6 +23,9 @@ */ package org.modeshape.connector.meta.jdbc; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNull.notNullValue; +import static org.junit.Assert.assertThat; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -36,7 +39,8 @@ public class TestEnvironment { static Properties propertiesFor( Object testCase ) { Properties properties = new Properties(); - ClassLoader loader = testCase instanceof Class ? ((Class)testCase).getClassLoader() : testCase.getClass().getClassLoader(); + ClassLoader loader = testCase instanceof Class ? ((Class)testCase).getClassLoader() : testCase.getClass() + .getClassLoader(); try { properties.load(loader.getResourceAsStream("database.properties")); } catch (IOException e) { @@ -107,7 +111,7 @@ public class TestEnvironment { Object testCase ) throws Exception { Connection conn = null; Statement stmt = null; - InputStream is = null; + InputStream istream = null; BufferedReader reader = null; try { @@ -115,8 +119,9 @@ public class TestEnvironment { conn = dataSource.getConnection(); stmt = conn.createStatement(); - is = TestEnvironment.class.getResourceAsStream("/" + properties.getProperty("database") + "/" + fileName); - reader = new BufferedReader(new InputStreamReader(is)); + istream = TestEnvironment.class.getResourceAsStream("/" + properties.getProperty("database") + "/" + fileName); + assertThat(istream, is(notNullValue())); + reader = new BufferedReader(new InputStreamReader(istream)); /* * We have to send the DDL line-at-a-time because the MySQL driver doesn't like getting multiple DDL statements at once @@ -139,8 +144,8 @@ public class TestEnvironment { conn.close(); } catch (Exception ignore) { } - if (is != null) try { - is.close(); + if (istream != null) try { + istream.close(); } catch (Exception ignore) { } } Index: extensions/modeshape-connector-jdbc-metadata/src/test/resources/mysql5_local/create.ddl new file mode 100644 =================================================================== --- /dev/null (revision 2666) +++ extensions/modeshape-connector-jdbc-metadata/src/test/resources/mysql5_local/create.ddl (working copy) @@ -0,0 +1,28 @@ +-- ModeShape (http://www.modeshape.org) +-- See the COPYRIGHT.txt file distributed with this work for information +-- regarding copyright ownership. Some portions may be licensed +-- to Red Hat, Inc. under one or more contributor license agreements. +-- See the AUTHORS.txt file in the distribution for a full listing of +-- individual contributors. +-- +-- ModeShape is free software. Unless otherwise indicated, all code in ModeShape +-- is licensed to you under the terms of the GNU Lesser General Public License as +-- published by the Free Software Foundation; either version 2.1 of +-- the License, or (at your option) any later version. +-- +-- ModeShape 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 this software; if not, write to the Free +-- Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +-- 02110-1301 USA, or see the FSF site: http://www.fsf.org. + +CREATE TABLE IF NOT EXISTS chain (ID INT NOT NULL PRIMARY KEY, NAME VARCHAR(30) NOT NULL) TYPE=INNODB; +CREATE TABLE IF NOT EXISTS area (ID INT NOT NULL PRIMARY KEY, NAME VARCHAR(30) NOT NULL, CHAIN_ID INT NOT NULL) TYPE=INNODB; +CREATE TABLE IF NOT EXISTS region (ID INT NOT NULL PRIMARY KEY, NAME VARCHAR(30) NOT NULL, AREA_ID INT NOT NULL) TYPE=INNODB; +CREATE TABLE IF NOT EXISTS district (ID INT NOT NULL PRIMARY KEY, NAME VARCHAR(30) NOT NULL, REGION_ID INT NOT NULL) TYPE=INNODB; + +CREATE TABLE IF NOT EXISTS sales (ID INT NOT NULL, SALES_DATE DATE NOT NULL, DISTRICT_ID INT NOT NULL, AMOUNT INT NULL) TYPE=INNODB; Index: extensions/modeshape-connector-jdbc-metadata/src/test/resources/mysql5_local/drop.ddl new file mode 100644 =================================================================== --- /dev/null (revision 2666) +++ extensions/modeshape-connector-jdbc-metadata/src/test/resources/mysql5_local/drop.ddl (working copy) @@ -0,0 +1,27 @@ +-- ModeShape (http://www.modeshape.org) +-- See the COPYRIGHT.txt file distributed with this work for information +-- regarding copyright ownership. Some portions may be licensed +-- to Red Hat, Inc. under one or more contributor license agreements. +-- See the AUTHORS.txt file in the distribution for a full listing of +-- individual contributors. +-- +-- ModeShape is free software. Unless otherwise indicated, all code in ModeShape +-- is licensed to you under the terms of the GNU Lesser General Public License as +-- published by the Free Software Foundation; either version 2.1 of +-- the License, or (at your option) any later version. +-- +-- ModeShape 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 this software; if not, write to the Free +-- Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +-- 02110-1301 USA, or see the FSF site: http://www.fsf.org. + +DROP TABLE sales; +DROP TABLE district; +DROP TABLE region; +DROP TABLE area; +DROP TABLE chain; Index: extensions/modeshape-connector-jdbc-metadata/src/test/resources/postgresql_local/create.ddl new file mode 100644 =================================================================== --- /dev/null (revision 2666) +++ extensions/modeshape-connector-jdbc-metadata/src/test/resources/postgresql_local/create.ddl (working copy) @@ -0,0 +1,33 @@ +-- ModeShape (http://www.modeshape.org) +-- See the COPYRIGHT.txt file distributed with this work for information +-- regarding copyright ownership. Some portions may be licensed +-- to Red Hat, Inc. under one or more contributor license agreements. +-- See the AUTHORS.txt file in the distribution for a full listing of +-- individual contributors. +-- +-- ModeShape is free software. Unless otherwise indicated, all code in ModeShape +-- is licensed to you under the terms of the GNU Lesser General Public License as +-- published by the Free Software Foundation; either version 2.1 of +-- the License, or (at your option) any later version. +-- +-- ModeShape 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 this software; if not, write to the Free +-- Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +-- 02110-1301 USA, or see the FSF site: http://www.fsf.org. + +CREATE TABLE chain (ID INTEGER NOT NULL PRIMARY KEY, NAME CHAR(30) NOT NULL); +CREATE TABLE area (ID INTEGER NOT NULL PRIMARY KEY, NAME CHAR(30) NOT NULL, CHAIN_ID INTEGER NOT NULL); +CREATE TABLE region (ID INTEGER NOT NULL PRIMARY KEY, NAME CHAR(30) NOT NULL, AREA_ID INTEGER NOT NULL); +CREATE TABLE district (ID INTEGER NOT NULL PRIMARY KEY, NAME CHAR(30) NOT NULL, REGION_ID INTEGER NOT NULL); + +CREATE TABLE sales (ID INTEGER NOT NULL, SALES_DATE DATE NOT NULL, DISTRICT_ID INTEGER NOT NULL, AMOUNT INTEGER NULL); +ALTER TABLE sales ADD CONSTRAINT PK_SALES PRIMARY KEY (ID, SALES_DATE); + +ALTER TABLE area ADD CONSTRAINT FK_CHAIN FOREIGN KEY(CHAIN_ID) REFERENCES CHAIN(ID) ON DELETE CASCADE; +ALTER TABLE region ADD CONSTRAINT FK_AREA FOREIGN KEY(AREA_ID) REFERENCES AREA(ID) ON DELETE CASCADE; +ALTER TABLE district ADD CONSTRAINT FK_REGION FOREIGN KEY(REGION_ID) REFERENCES REGION(ID) ON DELETE CASCADE; Index: extensions/modeshape-connector-jdbc-metadata/src/test/resources/postgresql_local/drop.ddl new file mode 100644 =================================================================== --- /dev/null (revision 2666) +++ extensions/modeshape-connector-jdbc-metadata/src/test/resources/postgresql_local/drop.ddl (working copy) @@ -0,0 +1,27 @@ +-- ModeShape (http://www.modeshape.org) +-- See the COPYRIGHT.txt file distributed with this work for information +-- regarding copyright ownership. Some portions may be licensed +-- to Red Hat, Inc. under one or more contributor license agreements. +-- See the AUTHORS.txt file in the distribution for a full listing of +-- individual contributors. +-- +-- ModeShape is free software. Unless otherwise indicated, all code in ModeShape +-- is licensed to you under the terms of the GNU Lesser General Public License as +-- published by the Free Software Foundation; either version 2.1 of +-- the License, or (at your option) any later version. +-- +-- ModeShape 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 this software; if not, write to the Free +-- Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +-- 02110-1301 USA, or see the FSF site: http://www.fsf.org. + +DROP TABLE sales; +DROP TABLE district; +DROP TABLE region; +DROP TABLE area; +DROP TABLE chain; Index: extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/JpaSource.java =================================================================== --- extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/JpaSource.java (revision 2666) +++ extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/JpaSource.java (working copy) @@ -141,6 +141,10 @@ public class JpaSource implements RepositorySource, ObjectFactory { */ protected static final boolean SUPPORTS_EVENTS = true; /** + * This source does not support automatic garbage collection. + */ + protected static final boolean SUPPORTS_AUTOMATIC_GARBAGE_COLLECTION = false; + /** * This source supports same-name-siblings. */ protected static final boolean SUPPORTS_SAME_NAME_SIBLINGS = true; @@ -333,7 +337,7 @@ public class JpaSource implements RepositorySource, ObjectFactory { DEFAULT_ALLOWS_UPDATES, SUPPORTS_EVENTS, DEFAULT_SUPPORTS_CREATING_WORKSPACES, - SUPPORTS_REFERENCES); + SUPPORTS_REFERENCES).withAutomaticGarbageCollection(SUPPORTS_AUTOMATIC_GARBAGE_COLLECTION); @Description( i18n = JpaConnectorI18n.class, value = "modelNamePropertyDescription" ) @Label( i18n = JpaConnectorI18n.class, value = "modelNamePropertyLabel" ) Index: extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/LargeValueEntity.java =================================================================== --- extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/LargeValueEntity.java (revision 2666) +++ extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/LargeValueEntity.java (working copy) @@ -24,7 +24,6 @@ package org.modeshape.connector.store.jpa.model.simple; import java.security.NoSuchAlgorithmException; -import java.util.List; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.EntityManager; @@ -32,7 +31,6 @@ import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.Id; import javax.persistence.Lob; -import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import javax.persistence.Query; import javax.persistence.Table; @@ -48,12 +46,33 @@ import org.modeshape.graph.property.PropertyType; */ @Entity @Table( name = "MODE_SIMPLE_LARGE_VALUES" ) -@NamedQueries( { - @NamedQuery( name = "LargeValueEntity.selectUnused", query = "select largeValue.hash from LargeValueEntity largeValue where largeValue.hash not in (select values.hash from NodeEntity node join node.largeValues values)" ), - @NamedQuery( name = "LargeValueEntity.deleteAllUnused", query = "delete LargeValueEntity lve where lve.hash not in (select values.hash from NodeEntity node join node.largeValues values)" ), - @NamedQuery( name = "LargeValueEntity.deleteIn", query = "delete LargeValueEntity lve where lve.hash in (:inValues)" )} ) +@NamedQuery( name = "LargeValueEntity.deleteAllUnused", query = "delete LargeValueEntity lve where lve.hash not in (select values.hash from NodeEntity node join node.largeValues values)" ) public class LargeValueEntity { + /** + * The MySQL delete statement to remove all unused LargeValueEntity records. + *

+ * Normally, the "LargeValueEntity.deleteAllUnused" named query attempts to delete all of the LargeValueEntity records that + * have an SHA1 that is not in the set of SHA1 values in the "usages" intersect table. This works well on all DBMSes except + * for MySQL, which is unable to select from the table being deleted (see MODE-691). Therefore, we use a MySQL-specific native + * query to do this deletion. + *

+ *

+ * This delete statement uses a left outer join between the LargeValueEntity and "usages" table and a criteria on the result + * to ensure that the only records returned are those without any "usages" records in the tuples. This works because a left + * outer join between tables A and B always contains all records from A, whether or not there are no corresponding records + * from B. And, if a criteria is added such that a column from B that can never be null is actually null, then we know the + * result will contain only those records from A that do not have a corresponding B record. In our case, this ends up + * deleting only those LargeValueEntity records that are not referenced in the "usages" table (i.e., they are not used). + *

+ *

+ * We can do this native SQL because this only is used for the MySQL dialect. + *

+ */ + private static final String MYSQL_DELETE_ALL_UNUSED_LARGE_VALUE_ENTITIES = "DELETE lv FROM MODE_SIMPLE_LARGE_VALUES AS lv " + + "LEFT OUTER JOIN ModeShape_LARGEVALUE_USAGES AS lvu " + + "ON lv.SHA1 = lvu.largeValues_SHA1 WHERE lvu.ID IS NULL"; + @Id @Column( name = "SHA1", nullable = false, length = 40 ) private String hash; @@ -189,43 +208,29 @@ public class LargeValueEntity { * * @param manager the manager; never null * @param dialect the dialect - * @return the number of deleted large values + * @return whether all the unused records were able to be removed in this pass */ - @SuppressWarnings( "unchecked" ) - public static int deleteUnused( EntityManager manager, - String dialect ) { + public static boolean deleteUnused( EntityManager manager, + String dialect ) { assert manager != null; - int result = 0; - if (dialect != null && dialect.toLowerCase().indexOf("mysql") != -1) { - // Unfortunately, we cannot delete all the unused large values in a single statement - // because of MySQL (see MODE-691). Therefore, we need to do this in multiple steps: - // 1) Find the set of hashes that are not used anymore - // 2) Delete each of these rows, using bulk deletes with a small number (20) of hashes at a time - - Query select = manager.createNamedQuery("LargeValueEntity.selectUnused"); - List hashes = select.getResultList(); - if (hashes.isEmpty()) return 0; - - // Delete the unused large entities, (up to) 20 at a time - int endIndex = hashes.size(); - int fromIndex = 0; - do { - int toIndex = Math.min(fromIndex + 20, endIndex); - Query query = manager.createNamedQuery("LargeValueEntity.deleteIn"); - query.setParameter("inValues", hashes.subList(fromIndex, toIndex)); - query.executeUpdate(); - result += toIndex - fromIndex; - fromIndex = toIndex; - } while (fromIndex < endIndex); - } else { + try { + if (dialect != null && dialect.toLowerCase().indexOf("mysql") != -1) { + // Because MySQL does not allow DELETE statements with subselects against the same table, + // we have to use a different approach here. There's really no effective way to do this + // in HQL, but since we're only dealing with MySQL, we can drop to native SQL and + // do a very efficient delete ... + Query delete = manager.createNativeQuery(MYSQL_DELETE_ALL_UNUSED_LARGE_VALUE_ENTITIES); + delete.executeUpdate(); + return true; + } // For all dialects other than MySQL, we can just use the one delete statement ... Query delete = manager.createNamedQuery("LargeValueEntity.deleteAllUnused"); - result = delete.executeUpdate(); + delete.executeUpdate(); + return true; + } finally { + manager.flush(); } - - manager.flush(); - return result; } private static byte[] computeHash( byte[] value ) { Index: extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/SimpleJpaRepository.java =================================================================== --- extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/SimpleJpaRepository.java (revision 2666) +++ extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/SimpleJpaRepository.java (working copy) @@ -245,6 +245,17 @@ public class SimpleJpaRepository extends MapRepository { } /** + * Collect and remove all records that are no longer needed in the database. + * + * @return true if all unused records could be deleted, or false if some unused records still remain (because removing them + * all would have been too time consuming or costly). + */ + protected boolean collectGarbage() { + // Delete unused large values ... + return LargeValueEntity.deleteUnused(entityManager, dialect); + } + + /** * This class provides a logical mapping of UUIDs to {@link JpaNode nodes} within a named workspace. *

* Like its enclosing class, this class only survives for the lifetime of a single request (which may be a @@ -409,9 +420,6 @@ public class SimpleJpaRepository extends MapRepository { branch.deleteSubgraph(true); branch.close(); - // Delete unused large values ... - LargeValueEntity.deleteUnused(entityManager, dialect); - // And clean up the local cache by paths by brute force ... this.nodesByPath.clear(); } @@ -433,9 +441,6 @@ public class SimpleJpaRepository extends MapRepository { Query query = entityManager.createQuery("NodeEntity.deleteAllInWorkspace"); query.setParameter("workspaceId", workspaceId); query.executeUpdate(); - - // Delete unused large values ... - LargeValueEntity.deleteUnused(entityManager, dialect); } /* Index: extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/SimpleRequestProcessor.java =================================================================== --- extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/SimpleRequestProcessor.java (revision 2666) +++ extensions/modeshape-connector-store-jpa/src/main/java/org/modeshape/connector/store/jpa/model/simple/SimpleRequestProcessor.java (working copy) @@ -15,6 +15,7 @@ import org.modeshape.graph.property.Path; import org.modeshape.graph.property.PathFactory; import org.modeshape.graph.property.PathNotFoundException; import org.modeshape.graph.request.CloneWorkspaceRequest; +import org.modeshape.graph.request.CollectGarbageRequest; import org.modeshape.graph.request.CreateWorkspaceRequest; import org.modeshape.graph.request.InvalidRequestException; import org.modeshape.graph.request.ReadBranchRequest; @@ -161,4 +162,16 @@ public class SimpleRequestProcessor extends MapRequestProcessor { super.process(request); } + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.request.processor.RequestProcessor#process(org.modeshape.graph.request.CollectGarbageRequest) + */ + @Override + public void process( CollectGarbageRequest request ) { + boolean additionalPassRequired = !repository.collectGarbage(); + request.setAdditionalPassRequired(additionalPassRequired); + super.process(request); + } + } Index: extensions/modeshape-connector-store-jpa/src/test/java/org/modeshape/connector/store/jpa/JdbcConnectionTest.java =================================================================== --- extensions/modeshape-connector-store-jpa/src/test/java/org/modeshape/connector/store/jpa/JdbcConnectionTest.java (revision 2666) +++ extensions/modeshape-connector-store-jpa/src/test/java/org/modeshape/connector/store/jpa/JdbcConnectionTest.java (working copy) @@ -127,7 +127,12 @@ public class JdbcConnectionTest { connection = source.getConnection(); assertThat(source.getDialect(), is(notNullValue())); if (expectedDialect != null) { - assertThat(source.getDialect(), is(expectedDialect)); + if (expectedDialect.toLowerCase().contains("mysql")) { + // The MySQL auto-detected dialect may be different than the dialect that was explicitly set + assertThat(source.getDialect().toLowerCase().contains("mysql"), is(true)); + } else { + assertThat(source.getDialect(), is(expectedDialect)); + } } } finally { if (connection != null) connection.close(); Index: extensions/modeshape-connector-store-jpa/src/test/java/org/modeshape/connector/store/jpa/JpaConnectorWritingTest.java =================================================================== --- extensions/modeshape-connector-store-jpa/src/test/java/org/modeshape/connector/store/jpa/JpaConnectorWritingTest.java (revision 2666) +++ extensions/modeshape-connector-store-jpa/src/test/java/org/modeshape/connector/store/jpa/JpaConnectorWritingTest.java (working copy) @@ -24,17 +24,15 @@ package org.modeshape.connector.store.jpa; import java.util.UUID; +import org.junit.Test; +import org.modeshape.common.FixFor; import org.modeshape.common.statistic.Stopwatch; import org.modeshape.graph.Graph; import org.modeshape.graph.Location; import org.modeshape.graph.connector.RepositorySource; import org.modeshape.graph.connector.test.WritableConnectorTest; import org.modeshape.graph.property.ReferentialIntegrityException; -import org.junit.Test; -/** - * @author Randall Hauch - */ public class JpaConnectorWritingTest extends WritableConnectorTest { private boolean isReferentialIntegrityEnforced = false; @@ -49,6 +47,8 @@ public class JpaConnectorWritingTest extends WritableConnectorTest { // Set the connection properties using the environment defined in the POM files ... JpaSource source = TestEnvironment.configureJpaSource("Test Repository", this); + source.setLargeValueSizeInBytes(100L); + // Override the inherited properties ... source.setReferentialIntegrityEnforced(isReferentialIntegrityEnforced); source.setCompressData(true); @@ -119,4 +119,49 @@ public class JpaConnectorWritingTest extends WritableConnectorTest { .into("/newUuids") .replacingExistingNodesWithSameUuids(); } + + @FixFor( {"MODE-1071", "MODE-1066"} ) + @Test + public void shouldSuccessfullyCollectGarbageOnePassAfterCreatingContentButNotDeletingAnyNodes() { + String workspaceName = "copyChildrenSource"; + tryCreatingAWorkspaceNamed(workspaceName); + + graph.useWorkspace(workspaceName); + String initialPath = ""; + int depth = 4; + int numChildrenPerNode = 3; + int numPropertiesPerNode = 10; + Stopwatch sw = new Stopwatch(); + boolean batch = true; + useLargeValues = true; + createSubgraph(graph, initialPath, depth, numChildrenPerNode, numPropertiesPerNode, batch, sw, System.out, null); + + int passes = 1; + collectGarbage(passes); + } + + @FixFor( {"MODE-1071", "MODE-1066"} ) + @Test + public void shouldSuccessfullyCollectGarbageMultiplePassesAfterCreatingContentAndDeletingSome() { + String workspaceName = "copyChildrenSource"; + tryCreatingAWorkspaceNamed(workspaceName); + + graph.useWorkspace(workspaceName); + String initialPath = ""; + int depth = 4; + int numChildrenPerNode = 6; + int numPropertiesPerNode = 10; + Stopwatch sw = new Stopwatch(); + boolean batch = true; + useLargeValues = true; + useUniqueLargeValues = true; + createSubgraph(graph, initialPath, depth, numChildrenPerNode, numPropertiesPerNode, batch, sw, System.out, null); + + collectGarbage(1); + + graph.delete("/node2").and(); + + int passes = 3; + collectGarbage(passes); + } } Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/RepositorySourceCapabilities.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/connector/RepositorySourceCapabilities.java (revision 2666) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/RepositorySourceCapabilities.java (working copy) @@ -24,6 +24,7 @@ package org.modeshape.graph.connector; import net.jcip.annotations.Immutable; +import org.modeshape.graph.request.CollectGarbageRequest; /** * The capabilities of a {@link RepositorySource}. This class can be used as is, or subclassed by a connector to define more @@ -74,6 +75,11 @@ public class RepositorySourceCapabilities { */ public static final boolean DEFAULT_SUPPORT_LOCKS = false; + /** + * The default support for automatic garbage collection is {@value} . + */ + public static final boolean DEFAULT_SUPPORT_AUTOMATIC_GARBAGE_COLLECTION = true; + private final boolean sameNameSiblings; private final boolean updates; private final boolean events; @@ -82,6 +88,7 @@ public class RepositorySourceCapabilities { private final boolean locks; private final boolean queries; private final boolean searches; + private final boolean autoGarbageCollection; /** * Create a capabilities object using the defaults, . @@ -89,19 +96,20 @@ public class RepositorySourceCapabilities { public RepositorySourceCapabilities() { this(DEFAULT_SUPPORT_SAME_NAME_SIBLINGS, DEFAULT_SUPPORT_UPDATES, DEFAULT_SUPPORT_EVENTS, DEFAULT_SUPPORT_CREATING_WORKSPACES, DEFAULT_SUPPORT_REFERENCES, DEFAULT_SUPPORT_LOCKS, DEFAULT_SUPPORT_QUERIES, - DEFAULT_SUPPORT_SEARCHES); + DEFAULT_SUPPORT_SEARCHES, DEFAULT_SUPPORT_AUTOMATIC_GARBAGE_COLLECTION); } public RepositorySourceCapabilities( RepositorySourceCapabilities capabilities ) { this(capabilities.supportsSameNameSiblings(), capabilities.supportsUpdates(), capabilities.supportsEvents(), capabilities.supportsCreatingWorkspaces(), capabilities.supportsReferences(), capabilities.supportsLocks(), - capabilities.supportsQueries(), capabilities.supportsSearches()); + capabilities.supportsQueries(), capabilities.supportsSearches(), capabilities.supportsAutomaticGarbageCollection()); } public RepositorySourceCapabilities( boolean supportsSameNameSiblings, boolean supportsUpdates ) { this(supportsSameNameSiblings, supportsUpdates, DEFAULT_SUPPORT_EVENTS, DEFAULT_SUPPORT_CREATING_WORKSPACES, - DEFAULT_SUPPORT_REFERENCES, DEFAULT_SUPPORT_LOCKS, DEFAULT_SUPPORT_QUERIES, DEFAULT_SUPPORT_SEARCHES); + DEFAULT_SUPPORT_REFERENCES, DEFAULT_SUPPORT_LOCKS, DEFAULT_SUPPORT_QUERIES, DEFAULT_SUPPORT_SEARCHES, + DEFAULT_SUPPORT_AUTOMATIC_GARBAGE_COLLECTION); } public RepositorySourceCapabilities( boolean supportsSameNameSiblings, @@ -110,7 +118,8 @@ public class RepositorySourceCapabilities { boolean supportsCreatingWorkspaces, boolean supportsReferences ) { this(supportsSameNameSiblings, supportsUpdates, supportsEvents, supportsCreatingWorkspaces, supportsReferences, - DEFAULT_SUPPORT_LOCKS, DEFAULT_SUPPORT_QUERIES, DEFAULT_SUPPORT_SEARCHES); + DEFAULT_SUPPORT_LOCKS, DEFAULT_SUPPORT_QUERIES, DEFAULT_SUPPORT_SEARCHES, + DEFAULT_SUPPORT_AUTOMATIC_GARBAGE_COLLECTION); } public RepositorySourceCapabilities( boolean supportsSameNameSiblings, @@ -120,7 +129,8 @@ public class RepositorySourceCapabilities { boolean supportsReferences, boolean supportsLocks, boolean supportsQueries, - boolean supportsSearches ) { + boolean supportsSearches, + boolean supportsAutomaticGarbageCollection ) { this.sameNameSiblings = supportsSameNameSiblings; this.updates = supportsUpdates; @@ -130,6 +140,7 @@ public class RepositorySourceCapabilities { this.locks = supportsLocks; this.queries = supportsQueries; this.searches = supportsSearches; + this.autoGarbageCollection = supportsAutomaticGarbageCollection; } /** @@ -205,4 +216,117 @@ public class RepositorySourceCapabilities { public boolean supportsSearches() { return searches; } + + /** + * Return whether the source supports automatic garbage collection. If not, then the source expects explicit + * {@link CollectGarbageRequest} calls. + * + * @return true if automatic garbage collection is supported, or false if the source is not capable of automatically + * collecting all of its garbage and requires periodic, manual collection + */ + public boolean supportsAutomaticGarbageCollection() { + return autoGarbageCollection; + } + + /** + * Create a new instance that is a copy of this instance but uses the supplied value for {@link #supportsSameNameSiblings()}. + * + * @param sameNameSiblings the new value + * @return the new instance + */ + public RepositorySourceCapabilities withSameNameSiblings( boolean sameNameSiblings ) { + return new RepositorySourceCapabilities(sameNameSiblings, updates, events, creatingWorkspaces, references, locks, + queries, searches, autoGarbageCollection); + } + + /** + * Create a new instance that is a copy of this instance but uses the supplied value for {@link #supportsUpdates()}. + * + * @param updates the new value + * @return the new instance + */ + public RepositorySourceCapabilities withUpdates( boolean updates ) { + return new RepositorySourceCapabilities(sameNameSiblings, updates, events, creatingWorkspaces, references, locks, + queries, searches, autoGarbageCollection); + } + + /** + * Create a new instance that is a copy of this instance but uses the supplied value for {@link #supportsEvents()}. + * + * @param events the new value + * @return the new instance + */ + public RepositorySourceCapabilities withEvents( boolean events ) { + return new RepositorySourceCapabilities(sameNameSiblings, updates, events, creatingWorkspaces, references, locks, + queries, searches, autoGarbageCollection); + } + + /** + * Create a new instance that is a copy of this instance but uses the supplied value for {@link #supportsCreatingWorkspaces()} + * . + * + * @param creatingWorkspaces the new value + * @return the new instance + */ + public RepositorySourceCapabilities withCreatingWorkspaces( boolean creatingWorkspaces ) { + return new RepositorySourceCapabilities(sameNameSiblings, updates, events, creatingWorkspaces, references, locks, + queries, searches, autoGarbageCollection); + } + + /** + * Create a new instance that is a copy of this instance but uses the supplied value for {@link #supportsReferences()}. + * + * @param references the new value + * @return the new instance + */ + public RepositorySourceCapabilities withReferences( boolean references ) { + return new RepositorySourceCapabilities(sameNameSiblings, updates, events, creatingWorkspaces, references, locks, + queries, searches, autoGarbageCollection); + } + + /** + * Create a new instance that is a copy of this instance but uses the supplied value for {@link #supportsLocks()}. + * + * @param locks the new value + * @return the new instance + */ + public RepositorySourceCapabilities withLocks( boolean locks ) { + return new RepositorySourceCapabilities(sameNameSiblings, updates, events, creatingWorkspaces, references, locks, + queries, searches, autoGarbageCollection); + } + + /** + * Create a new instance that is a copy of this instance but uses the supplied value for {@link #supportsQueries()}. + * + * @param queries the new value + * @return the new instance + */ + public RepositorySourceCapabilities withQueries( boolean queries ) { + return new RepositorySourceCapabilities(sameNameSiblings, updates, events, creatingWorkspaces, references, locks, + queries, searches, autoGarbageCollection); + } + + /** + * Create a new instance that is a copy of this instance but uses the supplied value for {@link #supportsSearches()}. + * + * @param searches the new value + * @return the new instance + */ + public RepositorySourceCapabilities withSearches( boolean searches ) { + return new RepositorySourceCapabilities(sameNameSiblings, updates, events, creatingWorkspaces, references, locks, + queries, searches, autoGarbageCollection); + } + + /** + * Create a new instance that is a copy of this instance but uses the supplied value for + * {@link #supportsAutomaticGarbageCollection()}. + * + * @param autoGarbageCollection the new value + * @return the new instance + */ + public RepositorySourceCapabilities withAutomaticGarbageCollection( boolean autoGarbageCollection ) { + return new RepositorySourceCapabilities(sameNameSiblings, updates, events, creatingWorkspaces, references, locks, + queries, searches, autoGarbageCollection); + } + } Index: modeshape-graph/src/main/java/org/modeshape/graph/request/CollectGarbageRequest.java new file mode 100644 =================================================================== --- /dev/null (revision 2666) +++ modeshape-graph/src/main/java/org/modeshape/graph/request/CollectGarbageRequest.java (working copy) @@ -0,0 +1,109 @@ +/* + * ModeShape (http://www.modeshape.org) + * See the COPYRIGHT.txt file distributed with this work for information + * regarding copyright ownership. Some portions may be licensed + * to Red Hat, Inc. under one or more contributor license agreements. + * See the AUTHORS.txt file in the distribution for a full listing of + * individual contributors. + * + * Unless otherwise indicated, all code in ModeShape is licensed + * to you under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * ModeShape 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 this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.modeshape.graph.request; + +/** + * Request that garbage collection be performed. Processors may disregard this request. + */ +public final class CollectGarbageRequest extends Request { + + private static final long serialVersionUID = 1L; + + private boolean additionalPassRequired = false; + + /** + * Create a request to destroy an existing workspace. + */ + public CollectGarbageRequest() { + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.request.Request#isReadOnly() + */ + @Override + public boolean isReadOnly() { + return false; + } + + /** + * {@inheritDoc} + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return 1; + } + + /** + * {@inheritDoc} + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals( Object obj ) { + if (obj == this) return true; + if (this.getClass().isInstance(obj)) return true; + return false; + } + + /** + * After collecting garbage during one pass (per this request), set whether additional passes are still required. + * + * @param additionalPassRequired true if this pass did not collect all known gargabe and additional passes are required, or + * false otherwise + * @throws IllegalStateException if the request is frozen + */ + public void setAdditionalPassRequired( boolean additionalPassRequired ) { + checkNotFrozen(); + this.additionalPassRequired = additionalPassRequired; + } + + /** + * Determine whether additional garbage collection passes are still required after this pass. In other words, if 'true', then + * this pass did not completely collect all known garbage. + * + * @return true if this pass did not collect all known gargabe and additional passes are required, or false otherwise + */ + public boolean isAdditionalPassRequired() { + return additionalPassRequired; + } + + /** + * {@inheritDoc} + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "collect garbage"; + } + + @Override + public RequestType getType() { + return RequestType.COLLECT_GARBAGE; + } +} Index: modeshape-graph/src/main/java/org/modeshape/graph/request/RequestType.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/request/RequestType.java (revision 2666) +++ modeshape-graph/src/main/java/org/modeshape/graph/request/RequestType.java (working copy) @@ -8,6 +8,7 @@ public enum RequestType { COMPOSITE, CLONE_BRANCH, CLONE_WORKSPACE, + COLLECT_GARBAGE, COPY_BRANCH, CREATE_NODE, CREATE_WORKSPACE, Index: modeshape-graph/src/main/java/org/modeshape/graph/request/processor/LoggingRequestProcessor.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/request/processor/LoggingRequestProcessor.java (revision 2666) +++ modeshape-graph/src/main/java/org/modeshape/graph/request/processor/LoggingRequestProcessor.java (working copy) @@ -30,6 +30,7 @@ import org.modeshape.graph.GraphI18n; import org.modeshape.graph.request.AccessQueryRequest; import org.modeshape.graph.request.CloneBranchRequest; import org.modeshape.graph.request.CloneWorkspaceRequest; +import org.modeshape.graph.request.CollectGarbageRequest; import org.modeshape.graph.request.CompositeRequest; import org.modeshape.graph.request.CopyBranchRequest; import org.modeshape.graph.request.CreateNodeRequest; @@ -433,6 +434,18 @@ public class LoggingRequestProcessor extends RequestProcessor { /** * {@inheritDoc} * + * @see org.modeshape.graph.request.processor.RequestProcessor#process(org.modeshape.graph.request.CollectGarbageRequest) + */ + @Override + public void process( CollectGarbageRequest request ) { + LOGGER.log(level, GraphI18n.executingRequest, request); + delegate.process(request); + LOGGER.log(level, GraphI18n.executedRequest, request); + } + + /** + * {@inheritDoc} + * * @see org.modeshape.graph.request.processor.RequestProcessor#process(org.modeshape.graph.request.Request) */ @Override Index: modeshape-graph/src/main/java/org/modeshape/graph/request/processor/RequestProcessor.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/request/processor/RequestProcessor.java (revision 2666) +++ modeshape-graph/src/main/java/org/modeshape/graph/request/processor/RequestProcessor.java (working copy) @@ -51,6 +51,7 @@ import org.modeshape.graph.request.CacheableRequest; import org.modeshape.graph.request.ChangeRequest; import org.modeshape.graph.request.CloneBranchRequest; import org.modeshape.graph.request.CloneWorkspaceRequest; +import org.modeshape.graph.request.CollectGarbageRequest; import org.modeshape.graph.request.CompositeRequest; import org.modeshape.graph.request.CopyBranchRequest; import org.modeshape.graph.request.CreateNodeRequest; @@ -233,6 +234,9 @@ public abstract class RequestProcessor { case CLONE_WORKSPACE: process((CloneWorkspaceRequest)request); break; + case COLLECT_GARBAGE: + process((CollectGarbageRequest)request); + break; case COPY_BRANCH: process((CopyBranchRequest)request); break; @@ -1024,6 +1028,18 @@ public abstract class RequestProcessor { } /** + * Process a request to collect garbage. + *

+ * The default implementation of this method does nothing. + *

+ * + * @param request the request + */ + public void process( CollectGarbageRequest request ) { + // do nothing by default + } + + /** * Close this processor, allowing it to clean up any open resources. */ public void close() { Index: modeshape-graph/src/test/java/org/modeshape/graph/connector/test/AbstractConnectorTest.java =================================================================== --- modeshape-graph/src/test/java/org/modeshape/graph/connector/test/AbstractConnectorTest.java (revision 2666) +++ modeshape-graph/src/test/java/org/modeshape/graph/connector/test/AbstractConnectorTest.java (working copy) @@ -62,6 +62,7 @@ import org.modeshape.graph.property.Name; import org.modeshape.graph.property.Path; import org.modeshape.graph.property.Property; import org.modeshape.graph.property.ValueFormatException; +import org.modeshape.graph.request.CollectGarbageRequest; import org.modeshape.graph.request.ReadAllChildrenRequest; import org.modeshape.graph.request.ReadAllPropertiesRequest; import org.modeshape.graph.request.ReadNodeRequest; @@ -92,7 +93,10 @@ public abstract class AbstractConnectorTest { protected Observer observer; protected LinkedList allChanges; protected boolean print = false; + protected boolean useLargeValues = false; + protected boolean useUniqueLargeValues = false; protected PrintStream output = null; + protected long largeValueCounter = 0L; public void startRepository() throws Exception { if (!running) { @@ -646,7 +650,17 @@ public abstract class AbstractConnectorTest { String value = originalValue; for (int j = 0; j != numProps; ++j) { // value = value + originalValue; - create = create.with("property" + (j + 1), value); + if ((useLargeValues || useUniqueLargeValues) && i % 3 == 0) { + // Use a large value for some properties ... + String largeValue = originalValue; + for (int k = 0; k != 100; ++k) { + largeValue = largeValue + "(" + k + ")"; + } + if (useUniqueLargeValues) largeValue = "" + (++largeValueCounter) + largeValue; + create = create.with("property" + (j + 1), largeValue); + } else { + create = create.with("property" + (j + 1), value); + } } create.and(); } @@ -856,4 +870,19 @@ public abstract class AbstractConnectorTest { } return path(pathToExistingParent, nonExistentChildName); } + + protected void collectGarbage( int maxNumberOfPasses ) { + RepositoryConnection connection = connectionFactory.createConnection(source.getName()); + try { + for (int i = 0; i != maxNumberOfPasses; ++i) { + // And request garbage collection ... + CollectGarbageRequest request = new CollectGarbageRequest(); + connection.execute(context, request); + if (!request.isAdditionalPassRequired()) break; + } + } finally { + // Always close this connection after each pass ... + connection.close(); + } + } } Index: modeshape-integration-tests/src/test/java/org/modeshape/test/integration/jpa/JcrRepositoryWithJpaSourceTest.java =================================================================== --- modeshape-integration-tests/src/test/java/org/modeshape/test/integration/jpa/JcrRepositoryWithJpaSourceTest.java (revision 2666) +++ modeshape-integration-tests/src/test/java/org/modeshape/test/integration/jpa/JcrRepositoryWithJpaSourceTest.java (working copy) @@ -24,9 +24,14 @@ package org.modeshape.test.integration.jpa; import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNull.notNullValue; import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import java.io.IOException; +import java.io.InputStream; import java.net.URL; import javax.jcr.Credentials; +import javax.jcr.ImportUUIDBehavior; import javax.jcr.Node; import javax.jcr.Repository; import javax.jcr.RepositoryException; @@ -35,7 +40,9 @@ import javax.jcr.Workspace; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.modeshape.common.FixFor; import org.modeshape.graph.SecurityContext; +import org.modeshape.jcr.CndNodeTypeReader; import org.modeshape.jcr.JcrConfiguration; import org.modeshape.jcr.JcrEngine; import org.modeshape.jcr.JcrSecurityContextCredentials; @@ -139,6 +146,120 @@ public class JcrRepositoryWithJpaSourceTest { session2.logout(); } + @FixFor( "MODE-1066" ) + @Test + public void shouldReimportContentThatWasJustDeletedInPriorSave() throws Exception { + // Register the cars node type(s) ... + registerNodeTypes("io/cars.cnd", session); + + // Create a node under which we'll import the content ... + session.getRootNode().addNode("workArea"); + session.save(); + + // Import some content ... + InputStream stream = resourceStream("io/cars-system-view.xml"); + session.importXML("/workArea", stream, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW); + session.save(); + + // Delete the content ... + session.getNode("/workArea/Cars").remove(); + session.save(); + + // Import the same content ... + stream = resourceStream("io/cars-system-view.xml"); + session.importXML("/workArea", stream, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW); + session.save(); + } + + @FixFor( "MODE-1066" ) + @Test + public void shouldReimportContentThatWasJustDeletedInSameSave() throws Exception { + // Register the cars node type(s) ... + registerNodeTypes("io/cars.cnd", session); + + // Create a node under which we'll import the content ... + session.getRootNode().addNode("workArea"); + session.save(); + + for (int i = 0; i != 3; ++i) { + // Import some content ... + InputStream stream = resourceStream("io/cars-system-view.xml"); + session.importXML("/workArea", stream, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW); + + // Delete the content ... + session.getNode("/workArea/Cars").remove(); + } + session.save(); + } + + @FixFor( "MODE-1066" ) + @Test + public void shouldReimportContentWithUuidsThatWasJustDeletedInPriorSave() throws Exception { + // Register the cars node type(s) ... + registerNodeTypes("io/cars.cnd", session); + + // Create a node under which we'll import the content ... + session.getRootNode().addNode("workArea"); + session.save(); + + // Import some content ... + InputStream stream = resourceStream("io/cars-system-view-with-uuids.xml"); + session.importXML("/workArea", stream, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW); + session.save(); + + // Delete the content ... + session.getNode("/workArea/Cars").remove(); + session.save(); + + // Import the same content ... + stream = resourceStream("io/cars-system-view-with-uuids.xml"); + session.importXML("/workArea", stream, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW); + session.save(); + } + + @FixFor( "MODE-1066" ) + @Test + public void shouldReimportContentWithUuidsThatWasJustDeletedInSameSave() throws Exception { + // Register the cars node type(s) ... + registerNodeTypes("io/cars.cnd", session); + + // Create a node under which we'll import the content ... + session.getRootNode().addNode("workArea"); + session.save(); + + for (int i = 0; i != 3; ++i) { + // Import some content ... + InputStream stream = resourceStream("io/cars-system-view-with-uuids.xml"); + session.importXML("/workArea", stream, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW); + + // Delete the content ... + session.getNode("/workArea/Cars").remove(); + } + session.save(); + } + + protected void registerNodeTypes( String pathToCndResourceFile, + Session session ) throws IOException, RepositoryException { + // Register the cars node type(s) ... + CndNodeTypeReader reader = new CndNodeTypeReader(session); + reader.read(pathToCndResourceFile); + if (!reader.getProblems().isEmpty()) { + // Report problems + System.err.println(reader.getProblems()); + fail("Error loading the CND file at '" + pathToCndResourceFile + "'"); + } else { + boolean allowUpdate = false; + session.getWorkspace().getNodeTypeManager().registerNodeTypes(reader.getNodeTypeDefinitions(), allowUpdate); + } + + } + + protected InputStream resourceStream( String resourcePath ) { + InputStream stream = this.getClass().getClassLoader().getResourceAsStream(resourcePath); + assertThat(stream, is(notNullValue())); + return stream; + } + protected void assertAccessibleWorkspace( Session session, String workspaceName ) throws Exception { assertAccessibleWorkspace(session.getWorkspace(), workspaceName); Index: modeshape-integration-tests/src/test/java/org/modeshape/test/integration/sequencer/ddl/DdlSequencerIntegrationTest.java =================================================================== --- modeshape-integration-tests/src/test/java/org/modeshape/test/integration/sequencer/ddl/DdlSequencerIntegrationTest.java (revision 2666) +++ modeshape-integration-tests/src/test/java/org/modeshape/test/integration/sequencer/ddl/DdlSequencerIntegrationTest.java (working copy) @@ -391,6 +391,7 @@ public class DdlSequencerIntegrationTest extends DdlIntegrationTestUtil { // Upload a file ... uploadFile(ddlTestResourceRootFolder, "create_schema.ddl", "shouldBeAbleToCreateAndExecuteJcrSql2QueryWithOrderBy"); waitUntilSequencedNodesIs(1); + Thread.sleep(500); // Now query for content ... Query query = session.getWorkspace() Index: modeshape-integration-tests/src/test/resources/io/cars-system-view-with-uuids.xml new file mode 100644 =================================================================== --- /dev/null (revision 2666) +++ modeshape-integration-tests/src/test/resources/io/cars-system-view-with-uuids.xml (working copy) @@ -0,0 +1,470 @@ + + + + nt:unstructured + + + mix:referenceable + + + e41075cb-a09a-4910-87b1-90ce8b4ca9dd + + + + nt:unstructured + + + mix:referenceable + + + 7e999653-e558-4131-8889-af1e16872f4d + + + + car:Car + + + mix:referenceable + + + e92eddc1-d33a-4bd4-ae36-fe0a761b8d89 + + + 2008 + + + $21,500 + + + 45 + + + Prius + + + Toyota + + + 5 + + + 48 + + + 4 + + + + + car:Car + + + mix:referenceable + + + f6348fbe-a0ba-43c4-9ae5-3faff5c0f6ec + + + 2008 + + + $34,200 + + + 25 + + + Highlander + + + Toyota + + + 5 + + + 27 + + + 4 + + + + + car:Car + + + mix:referenceable + + + b79bd26a-3556-4c40-9c6a-bdcbecae7677 + + + Nissan + + + 23 + + + 2008 + + + $18,260 + + + 32 + + + Altima + + + + + + nt:unstructured + + + mix:referenceable + + + 9145c396-e6d2-46d7-a5c3-654b4094f762 + + + + car:Car + + + mix:referenceable + + + 6b595b19-7e89-4df5-954e-b2440b242403 + + + 5,935 cc 5.9 liters V 12 + + + 2008 + + + 108.0 + + + $171,600 + + + 19 + + + DB9 + + + Aston Martin + + + 12 + + + 5 + + + 185.5 + + + + + car:Car + + + mix:referenceable + + + 24807d85-6789-48f4-9e67-41c59d7d3fae + + + 2008 + + + $34,900 + + + 24 + + + G37 + + + Infiniti + + + 4 + + + 18 + + + 3 + + + + + + nt:unstructured + + + mix:referenceable + + + f96fe720-49a9-4bf2-9bc4-39a0dd90e374 + + + + car:Car + + + mix:referenceable + + + c91ed7ba-39c5-4fcd-854c-8efbb1567b95 + + + Cadillac + + + 3.6 liter V6 + + + 2008 + + + 1 + + + DTS + + + + + car:Car + + + mix:referenceable + + + baa8ad28-6707-4342-9314-bb82dd9067a9 + + + Bentley + + + 10 + + + 2008 + + + $170,990 + + + 17 + + + Continental + + + + + car:Car + + + mix:referenceable + + + 7c513b8e-e77e-40f2-bf4d-147419037a75 + + + 2008 + + + $36,305 + + + 25 + + + IS350 + + + Lexus + + + 5 + + + 18 + + + 4 + + + + + + nt:unstructured + + + mix:referenceable + + + 30568ebe-6351-4e74-9502-71b136387142 + + + + car:Car + + + mix:referenceable + + + df508fe2-439b-481c-9d29-4d7463471683 + + + 2008 + + + $33,985 + + + 23 + + + LR2 + + + Land Rover + + + 5 + + + 16 + + + 4 + + + + + car:Car + + + mix:referenceable + + + 745a0fa0-c8fb-4920-813f-7f677cb0149e + + + 2008 + + + $48,525 + + + 17 + + + LR3 + + + Land Rover + + + 2 + + + 12 + + + 5 + + + + + car:Car + + + mix:referenceable + + + b7b00901-abbf-4fab-8487-34f3d85f8d61 + + + 2008 + + + $30,595 + + + 16 + + + H3 + + + Hummer + + + 4 + + + 13 + + + 3 + + + + + car:Car + + + mix:referenceable + + + aea99057-3454-435b-aea5-1253cee1efd4 + + + 2008 + + + $23,910 + + + 20 + + + F-150 + + + Ford + + + 1 + + + 14 + + + 5 + + + + Index: modeshape-integration-tests/src/test/resources/io/cars-system-view.xml new file mode 100644 =================================================================== --- /dev/null (revision 2666) +++ modeshape-integration-tests/src/test/resources/io/cars-system-view.xml (working copy) @@ -0,0 +1,161 @@ + + + nt:unstructured + + nt:unstructured + + car:Car + Toyota + Prius + 2008 + $21,500 + 4 + 5 + 48 + 45 + + + car:Car + Toyota + Highlander + 2008 + $34,200 + 4 + 5 + 27 + 25 + + + car:Car + Nissan + Altima + 2008 + $18,260 + 23 + 32 + + + + nt:unstructured + + car:Car + Aston Martin + DB9 + 2008 + $171,600 + 5 + 12 + 19 + 185.5 + 108.0 + 5,935 cc 5.9 liters V 12 + + + car:Car + Infiniti + G37 + 2008 + $34,900 + 3 + 4 + 18 + 24 + + + + nt:unstructured + + car:Car + Cadillac + DTS + 2008 + 1 + 3.6 liter V6 + + + car:Car + Bentley + Continental + 2008 + $170,990 + 10 + 17 + + + car:Car + Lexus + IS350 + 2008 + $36,305 + 4 + 5 + 18 + 25 + + + + nt:unstructured + + car:Car + Land Rover + LR2 + 2008 + $33,985 + 4 + 5 + 16 + 23 + + + car:Car + Land Rover + LR3 + 2008 + $48,525 + 5 + 2 + 12 + 17 + + + car:Car + Hummer + H3 + 2008 + $30,595 + 3 + 4 + 13 + 16 + 5.7 L Vortec 5700 gasoline V8 TBI/GM 4L80-E 4-speed + + + car:Car + Ford + F-150 + 2008 + $23,910 + 5 + 1 + 14 + 20 + 4.6L 260 hp V8 5-speed (heavy-duty) + + + car:Car + Toyota + Land Cruiser FJ55 + 1967 + $13,255 + 5 + 3 + 7 + 9 + 3.9 L I6 F + + + Index: modeshape-integration-tests/src/test/resources/io/cars.cnd new file mode 100644 =================================================================== --- /dev/null (revision 2666) +++ modeshape-integration-tests/src/test/resources/io/cars.cnd (working copy) @@ -0,0 +1,50 @@ +/* + * ModeShape (http://www.modeshape.org) + * See the COPYRIGHT.txt file distributed with this work for information + * regarding copyright ownership. Some portions may be licensed + * to Red Hat, Inc. under one or more contributor license agreements. + * See the AUTHORS.txt file in the distribution for a full listing of + * individual contributors. + * + * ModeShape is free software. Unless otherwise indicated, all code in ModeShape + * is licensed to you under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * ModeShape 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 this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + +//------------------------------------------------------------------------------ +// N A M E S P A C E S +//------------------------------------------------------------------------------ + + + + + +//------------------------------------------------------------------------------ +// N O D E T Y P E S +//------------------------------------------------------------------------------ + +[car:Car] > nt:unstructured, mix:created + - car:maker (string) + - car:model (string) + - car:year (string) < '(19|20)\d{2}' // any 4 digit number starting with '19' or '20' + - car:msrp (string) < '[$]\d{1,3}[,]?\d{3}([.]\d{2})?' // of the form "$X,XXX.ZZ", "$XX,XXX.ZZ" or "$XXX,XXX.ZZ" + // where '.ZZ' is optional + - car:userRating (long) < '[1,5]' // any value from 1 to 5 (inclusive) + - car:valueRating (long) < '[1,5]' // any value from 1 to 5 (inclusive) + - car:mpgCity (long) < '(0,]' // any value greater than 0 + - car:mpgHighway (long) < '(0,]' // any value greater than 0 + - car:lengthInInches (double) < '(0,]' // any value greater than 0 + - car:wheelbaseInInches (double) < '(0,]' // any value greater than 0 + - car:engine (string) + Index: modeshape-repository/src/main/java/org/modeshape/repository/ModeShapeConfiguration.java =================================================================== --- modeshape-repository/src/main/java/org/modeshape/repository/ModeShapeConfiguration.java (revision 2666) +++ modeshape-repository/src/main/java/org/modeshape/repository/ModeShapeConfiguration.java (working copy) @@ -38,6 +38,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.Stack; +import java.util.concurrent.TimeUnit; import net.jcip.annotations.Immutable; import net.jcip.annotations.NotThreadSafe; import org.modeshape.common.collection.Problems; @@ -688,6 +689,29 @@ public class ModeShapeConfiguration { } /** + * Obtain the definition for this engine's global properties. + * + * @return the global properties definition; never null + */ + public GlobalProperties globalProperties() { + return globalProperties(this); + } + + /** + * Set the interval for garbage collection, should garbage collection be required by the sources). + * + * @param interval the interval magnitude + * @param unit the interval time unit + * @return this configuration object for method chaining purposes + */ + public ModeShapeConfiguration withGarbageCollectionInterval( long interval, + TimeUnit unit ) { + long intervalInSeconds = unit.convert(interval, TimeUnit.SECONDS); + globalProperties().setProperty(ModeShapeLexicon.GARBAGE_COLLECTION_INTERVAL, intervalInSeconds); + return this; + } + + /** * Convenience method to make the code that sets up this configuration easier to read. This method simply returns this object. * * @return this configuration component; never null @@ -1131,6 +1155,17 @@ public class ModeShapeConfiguration { return (ClusterDefinition)clusterDefinition; } + /** + * Utility method to construct a setter for global properties. + * + * @param the type of the return object + * @param returnObject the return object + * @return the global properties setter; never null + */ + protected GlobalProperties globalProperties( ReturnType returnObject ) { + return new GlobalProperties(returnObject, changes(), path()); + } + protected static class BaseReturnable implements Returnable { protected final ReturnType returnObject; @@ -1517,6 +1552,25 @@ public class ModeShapeConfiguration { } } + protected static class GlobalProperties extends GraphReturnable> { + protected GlobalProperties( ReturnType returnObject, + Graph.Batch batch, + Path path, + Name... names ) { + super(returnObject, batch, path, names); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.repository.ModeShapeConfiguration.GraphReturnable#thisType() + */ + @Override + protected GlobalProperties thisType() { + return this; + } + } + /** * Representation of the current configuration content. */ @@ -1663,5 +1717,15 @@ public class ModeShapeConfiguration { } return graph; } + + /** + * Read a global configuration property. + * + * @param name the property name + * @return the property, or null if there is no such property + */ + public Property getGlobalProperty( Name name ) { + return graph().getProperty(ModeShapeLexicon.GARBAGE_COLLECTION_INTERVAL).on(getPath()); + } } } Index: modeshape-repository/src/main/java/org/modeshape/repository/ModeShapeEngine.java =================================================================== --- modeshape-repository/src/main/java/org/modeshape/repository/ModeShapeEngine.java (revision 2666) +++ modeshape-repository/src/main/java/org/modeshape/repository/ModeShapeEngine.java (working copy) @@ -30,6 +30,8 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import net.jcip.annotations.Immutable; @@ -76,6 +78,12 @@ import org.modeshape.repository.sequencer.SequencingService; @Immutable public class ModeShapeEngine { + /** + * The default interval (in seconds) for running the garbage collection sweeps, if there are sources require it. The default + * value is 600 seconds, or 10 minutes. + */ + public static final int DEFAULT_GARBAGE_COLLECTION_INTERVAL_IN_SECONDS = 60 * 10; // every ten minutes + public static final String CONFIGURATION_REPOSITORY_NAME = "dna:configuration"; protected final ModeShapeConfiguration.ConfigurationDefinition configuration; @@ -91,6 +99,11 @@ public class ModeShapeEngine { private final String engineId = UUID.randomUUID().toString(); private final Logger logger; + /** + * Provides the ability to collect garbage periodically + */ + private final ScheduledExecutorService gcService = new ScheduledThreadPoolExecutor(1); + protected ModeShapeEngine( ExecutionContext context, ModeShapeConfiguration.ConfigurationDefinition configuration ) { this.problems = new SimpleProblems(); @@ -337,10 +350,47 @@ public class ModeShapeEngine { // Now register the repository service to be notified of changes to the configuration ... clusteringService.register(repositoryService); + // Start the periodic GC service + startGcService(); + // Check whether there are problems AFTER startup ... checkProblemsOnStartup(); } + protected void startGcService() { + if (!getRepositoryService().requiresGarbageCollection()) return; + + // Read the garbage collection interface from the configuration ... + long intervalInSeconds = DEFAULT_GARBAGE_COLLECTION_INTERVAL_IN_SECONDS; + Property intervalProp = configuration.getGlobalProperty(ModeShapeLexicon.GARBAGE_COLLECTION_INTERVAL); + if (intervalProp != null && !intervalProp.isEmpty()) { + try { + // This may throw an exception if the value is not a valid long ... + intervalInSeconds = context.getValueFactories().getLongFactory().create(intervalProp.getFirstValue()); + } catch (RuntimeException e) { + String actualValue = context.getValueFactories().getStringFactory().create(intervalProp.getFirstValue()); + problems.addError(RepositoryI18n.unableToUseGarbageCollectionIntervalValue, actualValue); + } + } + + final Runnable gcTask = new Runnable() { + public void run() { + getRepositoryService().runGarbageCollection(null); // log problems as errors + } + }; + try { + gcService.scheduleAtFixedRate(gcTask, intervalInSeconds, intervalInSeconds, TimeUnit.SECONDS); + checkProblemsOnStartup(); + } catch (RuntimeException e) { + try { + shutdown(); + } catch (Throwable t) { + // Don't care about these ... + } + throw e; + } + } + /** * Shutdown this engine to close all connections, terminate any ongoing background operations (such as sequencing), and * reclaim any resources that were acquired by this engine. This method may be called multiple times, but only the first time @@ -354,6 +404,9 @@ public class ModeShapeEngine { } protected void preShutdown() { + // Terminate the garbage collection thread ... + gcService.shutdown(); + // Terminate the executor service, which may be running background jobs that are not yet completed // and which will prevent new jobs being submitted (to the sequencing service) ... executorService.shutdown(); @@ -382,6 +435,7 @@ public class ModeShapeEngine { */ public boolean awaitTermination( long timeout, TimeUnit unit ) throws InterruptedException { + if (!gcService.awaitTermination(timeout, unit)) return false; if (!sequencingService.getAdministrator().awaitTermination(timeout, unit)) return false; if (!executorService.awaitTermination(timeout, unit)) return false; if (!repositoryService.getAdministrator().awaitTermination(timeout, unit)) return false; Index: modeshape-repository/src/main/java/org/modeshape/repository/ModeShapeLexicon.java =================================================================== --- modeshape-repository/src/main/java/org/modeshape/repository/ModeShapeLexicon.java (revision 2666) +++ modeshape-repository/src/main/java/org/modeshape/repository/ModeShapeLexicon.java (working copy) @@ -54,6 +54,7 @@ public class ModeShapeLexicon extends org.modeshape.graph.ModeShapeLexicon { public static final Name DERIVED = new BasicName(Namespace.URI, "derived"); public static final Name DERIVED_FROM = new BasicName(Namespace.URI, "derivedFrom"); public static final Name DERIVED_AT = new BasicName(Namespace.URI, "derivedAt"); + public static final Name GARBAGE_COLLECTION_INTERVAL = new BasicName(Namespace.URI, "gcInterval"); public static final Name INITIAL_CONTENT = new BasicName(Namespace.URI, "initialContent"); public static final Name CONTENT = new BasicName(Namespace.URI, "content"); Index: modeshape-repository/src/main/java/org/modeshape/repository/RepositoryI18n.java =================================================================== --- modeshape-repository/src/main/java/org/modeshape/repository/RepositoryI18n.java (revision 2666) +++ modeshape-repository/src/main/java/org/modeshape/repository/RepositoryI18n.java (working copy) @@ -39,6 +39,7 @@ public final class RepositoryI18n { public static I18n errorCreatingInstanceOfClassUsingClassLoaders; public static I18n errorSettingJavaBeanPropertyOnInstanceOfClass; public static I18n pathExpressionIsInvalidOnSequencer; + public static I18n unableToUseGarbageCollectionIntervalValue; // Services and Repository public static I18n invalidStateString; @@ -57,6 +58,7 @@ public final class RepositoryI18n { // Repository service ... public static I18n repositoryServiceName; + public static I18n errorCollectingGarbageInSource; // Clustering service ... public static I18n clusteringServiceName; Index: modeshape-repository/src/main/java/org/modeshape/repository/RepositoryService.java =================================================================== --- modeshape-repository/src/main/java/org/modeshape/repository/RepositoryService.java (revision 2666) +++ modeshape-repository/src/main/java/org/modeshape/repository/RepositoryService.java (working copy) @@ -25,12 +25,15 @@ package org.modeshape.repository; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.LinkedList; import java.util.Map; +import java.util.Queue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import net.jcip.annotations.ThreadSafe; import org.modeshape.common.collection.Problems; import org.modeshape.common.collection.SimpleProblems; +import org.modeshape.common.i18n.I18n; import org.modeshape.common.util.CheckArg; import org.modeshape.common.util.Logger; import org.modeshape.common.util.Reflection; @@ -40,7 +43,9 @@ import org.modeshape.graph.JcrLexicon; import org.modeshape.graph.Location; import org.modeshape.graph.Node; import org.modeshape.graph.Subgraph; +import org.modeshape.graph.connector.RepositoryConnection; import org.modeshape.graph.connector.RepositorySource; +import org.modeshape.graph.connector.RepositorySourceCapabilities; import org.modeshape.graph.observe.Changes; import org.modeshape.graph.observe.NetChangeObserver; import org.modeshape.graph.observe.ObservationBus; @@ -53,6 +58,7 @@ import org.modeshape.graph.property.Property; import org.modeshape.graph.property.PropertyType; import org.modeshape.graph.property.ValueFactories; import org.modeshape.graph.property.ValueFactory; +import org.modeshape.graph.request.CollectGarbageRequest; import org.modeshape.graph.request.ReadBranchRequest; import org.modeshape.repository.service.AbstractServiceAdministrator; import org.modeshape.repository.service.AdministeredService; @@ -64,10 +70,10 @@ import org.modeshape.repository.service.ServiceAdministrator; @ThreadSafe public class RepositoryService implements AdministeredService, Observer { + public static final int MAXIMUM_NUMBER_OF_PASSES_PER_GC_RUN = 10; + /** * The administrative component for this service. - * - * @author Randall Hauch */ protected class Administrator extends AbstractServiceAdministrator { @@ -530,6 +536,96 @@ public class RepositoryService implements AdministeredService, Observer { this.configurationChangeObserver.notify(changes); } + /** + * Determine if at least one source requires periodic garbage collection. + * + * @return true if {@link #runGarbageCollection(Problems)} should be called periodically, or false otherwise + */ + public boolean requiresGarbageCollection() { + for (RepositorySource source : getRepositoryLibrary().getSources()) { + if (!source.getCapabilities().supportsAutomaticGarbageCollection()) return true; + } + return false; + } + + /** + * This method goes through all {@link RepositorySource} instances in the {@link #getRepositoryLibrary() library}, and for + * each of them that don't {@link RepositorySourceCapabilities#supportsAutomaticGarbageCollection() support automatic garbage + * collection} will submit a {@link CollectGarbageRequest}. + *

+ * This method does all this work in the calling thread, blocking until all such requests have been issued and completed. It + * actually uses a queue, first enqueuing all RepositorySource instances that don't + * {@link RepositorySourceCapabilities#supportsAutomaticGarbageCollection() support automatic garbage collection}. It then + * pulls the first source from the queue, obtains a connection, submits a single {@link CollectGarbageRequest}, and + * re-enqueues the source {@link CollectGarbageRequest#isAdditionalPassRequired() if required}. However, this method never + * requests a source collect garbage more than {@link #MAXIMUM_NUMBER_OF_PASSES_PER_GC_RUN} times. + *

+ *

+ * Thus a source can implement a garbage collection sweep in a manner that does not require excess amount of time so as to not + * block other requests. After that pass is completed, the source can simply denote in the CollectGarbageRequest whether at + * least one additional GC pass should be performed. + *

+ * + * @param problems the problems container in which any errors should be reported; if null, then any problems will be logged + */ + public void runGarbageCollection( Problems problems ) { + final Logger logger = Logger.getLogger(getClass()); + Queue sourcesToGc = new LinkedList(); + for (RepositorySource source : getRepositoryLibrary().getSources()) { + if (source.getCapabilities().supportsAutomaticGarbageCollection()) continue; + sourcesToGc.add(new GarbageCollectedSource(source)); + } + + while (!sourcesToGc.isEmpty()) { + GarbageCollectedSource gcSource = sourcesToGc.poll(); + RepositorySource source = gcSource.source; + // Get a connection for this source ... + RepositoryConnection connection = getRepositoryLibrary().createConnection(source.getName()); + try { + // And request garbage collection ... + logger.debug("Garbage collection requested for {0}", source.getName()); + CollectGarbageRequest request = new CollectGarbageRequest(); + connection.execute(context, request); + gcSource.recordPass(); + if (request.isAdditionalPassRequired() && gcSource.hasPassesRemaining()) { + // This pass was not complete, so try to enqueue again ... + sourcesToGc.offer(gcSource); + logger.debug("Garbage collection partially completed for {0}; enqueuing again", source.getName()); + } else { + logger.debug("Garbage collection completed for {0}", source.getName()); + } + } catch (Throwable t) { + // Record this error and continue with the next source ... + I18n msg = RepositoryI18n.errorCollectingGarbageInSource; + if (problems != null) { + problems.addError(t, msg, source.getName(), t.getMessage()); + } else { + logger.error(msg, source.getName(), t.getMessage()); + } + } finally { + // Always close this connection after each pass ... + connection.close(); + } + } + } + + protected static class GarbageCollectedSource { + protected final RepositorySource source; + private int passesRemaining = MAXIMUM_NUMBER_OF_PASSES_PER_GC_RUN; + + protected GarbageCollectedSource( RepositorySource source ) { + this.source = source; + } + + protected void recordPass() { + --passesRemaining; + } + + protected boolean hasPassesRemaining() { + return passesRemaining > 0; + } + } + protected class ConfigurationChangeObserver extends NetChangeObserver { /** Index: modeshape-repository/src/main/resources/org/modeshape/repository/RepositoryI18n.properties =================================================================== --- modeshape-repository/src/main/resources/org/modeshape/repository/RepositoryI18n.properties (revision 2666) +++ modeshape-repository/src/main/resources/org/modeshape/repository/RepositoryI18n.properties (working copy) @@ -25,6 +25,7 @@ errorCreatingInstanceOfClass = Attempt to create instance of {0} resulted in err errorCreatingInstanceOfClassUsingClassLoaders = Attempt to create instance of {0} using classloader path {1} resulted in error: {2} errorSettingJavaBeanPropertyOnInstanceOfClass = Attempt to set {0} property on {1} instance resulted in error: {2} pathExpressionIsInvalidOnSequencer = The path expression "{0}" is invalid on the "{1}" sequencer: {2} +unableToUseGarbageCollectionIntervalValue = Unable to use the garbage collection value of "{0}"; expected a long literal value invalidStateString = Invalid state parameter "{0}" serviceShutdowAndMayNotBeStarted = The {0} has been shutdown and may not be (re)started @@ -41,6 +42,7 @@ errorFindingPropertyNameInPropertyChangedEvent = Error finding the name of the c errorFindingPropertyNameInPropertyRemovedEvent = Error finding the name of the removed property in the event path {0} repositoryServiceName = Repository Service +errorCollectingGarbageInSource = Error while collecting garbage in '{0}' source: {1} clusteringServiceName = Clustering Service clusteringConfigurationRequiresClusterName = Clustering configuration requires a non-blank 'clusterName' attribute. ModeShape instance will run in a local (non-clustered) mode. Index: pom.xml =================================================================== --- pom.xml (revision 2666) +++ pom.xml (working copy) @@ -525,6 +525,7 @@ hsqldb hsqldb + 1.8.0.2 test @@ -604,9 +605,10 @@ - org.h2database - h2database - 1.0.20061217 + com.h2database + h2 + 1.2.124 + test @@ -646,6 +648,33 @@ + + + mysql5_local + + + database + mysql5_local + + + + + mysql + mysql-connector-java + 5.1.7 + test + + + + mysql5 + org.hibernate.dialect.MySQL5InnoDBDialect + com.mysql.jdbc.Driver + jdbc:mysql://localhost:3306/test + unit_test + unit_test + + +