From: Kai Moritz Date: Wed, 16 Dec 2015 17:08:56 +0000 (+0100) Subject: Reworked configuration and the tracking thereof X-Git-Tag: hibernate-maven-plugin-2.0.0~15 X-Git-Url: http://juplo.de/gitweb/?a=commitdiff_plain;h=6dff3bfb0f9ea7a1d0cc56398aaad29e31a17b91;p=hibernate-maven-plugin Reworked configuration and the tracking thereof * Moved common parameters from CreateMojo to AbstractSchemaMojo * Reordered parameters into sensible groups * Renamed the maven-property-names of the parameters * All configuration-parameters are tracked, not only hibernate-parameters * Introduced special treatment for some of the plugin-parameters (export and show) --- diff --git a/src/it/hib-test/pom.xml b/src/it/hib-test/pom.xml index ba6f6b3f..25ee7a97 100644 --- a/src/it/hib-test/pom.xml +++ b/src/it/hib-test/pom.xml @@ -47,6 +47,7 @@ false + true diff --git a/src/it/hibernate4-maven-plugin-envers-sample/pom.xml b/src/it/hibernate4-maven-plugin-envers-sample/pom.xml index 5520ac2c..ef0f7935 100644 --- a/src/it/hibernate4-maven-plugin-envers-sample/pom.xml +++ b/src/it/hibernate4-maven-plugin-envers-sample/pom.xml @@ -188,17 +188,19 @@ ${project.build.directory}/test-classes/sql/create-tables-hsqldb.sql false + true create-drop-script - create + drop ${project.build.directory}/test-classes/sql/drop-tables-hsqldb.sql true + true diff --git a/src/it/properties/pom.xml b/src/it/properties/pom.xml index 7a9922e4..ac24f7f5 100644 --- a/src/it/properties/pom.xml +++ b/src/it/properties/pom.xml @@ -43,6 +43,9 @@ create + + true + diff --git a/src/it/schemaexport-example/schemaexport-example-persistence-impl/pom.xml b/src/it/schemaexport-example/schemaexport-example-persistence-impl/pom.xml index 1215a10c..8288131d 100644 --- a/src/it/schemaexport-example/schemaexport-example-persistence-impl/pom.xml +++ b/src/it/schemaexport-example/schemaexport-example-persistence-impl/pom.xml @@ -62,6 +62,7 @@ com.mysql.jdbc.Driver org.hibernate.dialect.MySQLDialect --> + true diff --git a/src/it/tutorials/annotations/pom.xml b/src/it/tutorials/annotations/pom.xml index 47c59916..86982ad2 100644 --- a/src/it/tutorials/annotations/pom.xml +++ b/src/it/tutorials/annotations/pom.xml @@ -60,6 +60,7 @@ true + true diff --git a/src/it/tutorials/basic/pom.xml b/src/it/tutorials/basic/pom.xml index 69731291..64ecf121 100644 --- a/src/it/tutorials/basic/pom.xml +++ b/src/it/tutorials/basic/pom.xml @@ -59,6 +59,7 @@ true + true diff --git a/src/it/tutorials/entitymanager/pom.xml b/src/it/tutorials/entitymanager/pom.xml index 3531d41b..9d740a5e 100644 --- a/src/it/tutorials/entitymanager/pom.xml +++ b/src/it/tutorials/entitymanager/pom.xml @@ -68,6 +68,7 @@ true org.hibernate.dialect.H2Dialect + true diff --git a/src/it/tutorials/envers/pom.xml b/src/it/tutorials/envers/pom.xml index 69eeb5af..f5d87692 100644 --- a/src/it/tutorials/envers/pom.xml +++ b/src/it/tutorials/envers/pom.xml @@ -73,6 +73,7 @@ true org.hibernate.dialect.H2Dialect + true diff --git a/src/it/tutorials/osgi/managed-jpa/pom.xml b/src/it/tutorials/osgi/managed-jpa/pom.xml index 1aa763c0..e26e99ed 100644 --- a/src/it/tutorials/osgi/managed-jpa/pom.xml +++ b/src/it/tutorials/osgi/managed-jpa/pom.xml @@ -81,6 +81,7 @@ jdbc:h2:mem:db1;MVCC=TRUE + true diff --git a/src/it/tutorials/osgi/unmanaged-jpa/pom.xml b/src/it/tutorials/osgi/unmanaged-jpa/pom.xml index 24b84635..c4c7cd0b 100644 --- a/src/it/tutorials/osgi/unmanaged-jpa/pom.xml +++ b/src/it/tutorials/osgi/unmanaged-jpa/pom.xml @@ -78,6 +78,9 @@ de.juplo hibernate-maven-plugin ${h4mp.version} + + true + diff --git a/src/it/tutorials/osgi/unmanaged-native/pom.xml b/src/it/tutorials/osgi/unmanaged-native/pom.xml index b83dea51..f91f11a0 100644 --- a/src/it/tutorials/osgi/unmanaged-native/pom.xml +++ b/src/it/tutorials/osgi/unmanaged-native/pom.xml @@ -86,6 +86,9 @@ de.juplo hibernate-maven-plugin ${h4mp.version} + + true + diff --git a/src/main/java/de/juplo/plugins/hibernate/AbstractSchemaMojo.java b/src/main/java/de/juplo/plugins/hibernate/AbstractSchemaMojo.java index 4135b618..b61d83c9 100644 --- a/src/main/java/de/juplo/plugins/hibernate/AbstractSchemaMojo.java +++ b/src/main/java/de/juplo/plugins/hibernate/AbstractSchemaMojo.java @@ -44,9 +44,12 @@ import org.hibernate.boot.registry.selector.spi.StrategySelector; import org.hibernate.boot.spi.MetadataImplementor; import static org.hibernate.cfg.AvailableSettings.DIALECT; import static org.hibernate.cfg.AvailableSettings.DRIVER; +import static org.hibernate.cfg.AvailableSettings.FORMAT_SQL; +import static org.hibernate.cfg.AvailableSettings.HBM2DLL_CREATE_NAMESPACES; import static org.hibernate.cfg.AvailableSettings.IMPLICIT_NAMING_STRATEGY; import static org.hibernate.cfg.AvailableSettings.PASS; import static org.hibernate.cfg.AvailableSettings.PHYSICAL_NAMING_STRATEGY; +import static org.hibernate.cfg.AvailableSettings.SHOW_SQL; import static org.hibernate.cfg.AvailableSettings.USER; import static org.hibernate.cfg.AvailableSettings.URL; import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; @@ -70,7 +73,13 @@ import org.scannotation.AnnotationDB; */ public abstract class AbstractSchemaMojo extends AbstractMojo { - public final static String EXPORT_SKIPPED_PROPERTY = "hibernate.export.skipped"; + public final static String EXPORT = "hibernate.schema.export"; + public final static String DELIMITER = "hibernate.schema.delimiter"; + public final static String OUTPUTDIRECTORY = "project.build.outputDirectory"; + public final static String SCAN_DEPENDENCIES = "hibernate.schema.scan.dependencies"; + public final static String SCAN_TESTCLASSES = "hibernate.schema.scan.test_classes"; + public final static String TEST_OUTPUTDIRECTORY = "project.build.testOutputDirectory"; + public final static String SKIPPED = "hibernate.schema.skipped"; private final static Pattern SPLIT = Pattern.compile("[^,\\s]+"); @@ -97,27 +106,144 @@ public abstract class AbstractSchemaMojo extends AbstractMojo */ String buildDirectory; + + /** Parameters to configure the genaration of the SQL *********************/ + /** - * Classes-Directory to scan. + * Export the database-schma to the database. + * If set to false, only the SQL-script is created and the + * database is not touched. *

- * This parameter defaults to the maven build-output-directory for classes. - * Additionally, all dependencies are scanned for annotated classes. + * Important: + * This configuration value can only be configured through the + * pom.xml, or by the definition of a system-property, because + * it is not known by Hibernate nor JPA and, hence, not picked up from + * their configuration! * - * @parameter property="project.build.outputDirectory" + * @parameter property="hibernate.schema.export" default-value="true" + * @since 2.0 + */ + Boolean export; + + /** + * Skip execution + *

+ * If set to true, the execution is skipped. + *

+ * A skipped execution is signaled via the maven-property + * ${hibernate.export.skipped}. + *

+ * The execution is skipped automatically, if no modified or newly added + * annotated classes are found and the dialect was not changed. + *

+ * Important: + * This configuration value can only be configured through the + * pom.xml, or by the definition of a system-property, because + * it is not known by Hibernate nor JPA and, hence, not picked up from + * their configuration! + * + * @parameter property="hibernate.schema.skip" default-value="${maven.test.skip}" * @since 1.0 */ - private String outputDirectory; + private boolean skip; /** - * Whether to scan test-classes too, or not. + * Force execution *

- * If this parameter is set to true the test-classes of the - * artifact will be scanned for hibernate-annotated classes additionally. + * Force execution, even if no modified or newly added annotated classes + * where found and the dialect was not changed. + *

+ * skip takes precedence over force. + *

+ * Important: + * This configuration value can only be configured through the + * pom.xml, or by the definition of a system-property, because + * it is not known by Hibernate nor JPA and, hence, not picked up from + * their configuration! * - * @parameter property="hibernate.export.scan_testclasses" default-value="false" - * @since 1.0.1 + * @parameter property="hibernate.schema.force" default-value="false" + * @since 1.0 + */ + private boolean force; + + /** + * Hibernate dialect. + * + * @parameter property="hibernate.dialect" + * @since 1.0 */ - private boolean scanTestClasses; + private String dialect; + + /** + * Delimiter in output-file. + *

+ * Important: + * This configuration value can only be configured through the + * pom.xml, or by the definition of a system-property, because + * it is not known by Hibernate nor JPA and, hence, not picked up from + * their configuration! + * + * @parameter property="hibernate.schema.delimiter" default-value=";" + * @since 1.0 + */ + String delimiter; + + /** + * Show the generated SQL in the command-line output. + * + * @parameter property="hibernate.show_sql" + * @since 1.0 + */ + Boolean show; + + /** + * Format output-file. + * + * @parameter property="hibernate.format_sql" + * @since 1.0 + */ + Boolean format; + + /** + * Specifies whether to automatically create also the database schema/catalog. + * + * @parameter property="hibernate.hbm2dll.create_namespaces" default-value="false" + * @since 2.0 + */ + Boolean createNamespaces; + + /** + * Implicit naming strategy + * + * @parameter property="hibernate.implicit_naming_strategy" + * @since 2.0 + */ + private String implicitNamingStrategy; + + /** + * Physical naming strategy + * + * @parameter property="hibernate.physical_naming_strategy" + * @since 2.0 + */ + private String physicalNamingStrategy; + + /** + * Classes-Directory to scan. + *

+ * This parameter defaults to the maven build-output-directory for classes. + * Additionally, all dependencies are scanned for annotated classes. + *

+ * Important: + * This configuration value can only be configured through the + * pom.xml, or by the definition of a system-property, because + * it is not known by Hibernate nor JPA and, hence, not picked up from + * their configuration! + * + * @parameter property="project.build.outputDirectory" + * @since 1.0 + */ + private String outputDirectory; /** * Dependency-Scopes, that should be scanned for annotated classes. @@ -125,7 +251,7 @@ public abstract class AbstractSchemaMojo extends AbstractMojo * By default, only dependencies in the scope compile are * scanned for annotated classes. Multiple scopes can be seperated by * white space or commas. - *

md5s + *

* If you do not want any dependencies to be scanned for annotated * classes, set this parameter to none. *

@@ -133,11 +259,28 @@ public abstract class AbstractSchemaMojo extends AbstractMojo * dependencies. If some of your annotated classes are hidden in a * transitive dependency, you can simply add that dependency explicitly. * - * @parameter property="hibernate.export.scan_dependencies" default-value="compile" + * @parameter property="hibernate.schema.scan.dependencies" default-value="compile" * @since 1.0.3 */ private String scanDependencies; + /** + * Whether to scan test-classes too, or not. + *

+ * If this parameter is set to true the test-classes of the + * artifact will be scanned for hibernate-annotated classes additionally. + *

+ * Important: + * This configuration value can only be configured through the + * pom.xml, or by the definition of a system-property, because + * it is not known by Hibernate nor JPA and, hence, not picked up from + * their configuration! + * + * @parameter property="hibernate.schema.scan.test_classes" default-value="false" + * @since 1.0.1 + */ + private Boolean scanTestClasses; + /** * Test-Classes-Directory to scan. *

@@ -146,40 +289,20 @@ public abstract class AbstractSchemaMojo extends AbstractMojo *

* This parameter is only used, when scanTestClasses is set * to true! + *

+ * Important: + * This configuration value can only be configured through the + * pom.xml, or by the definition of a system-property, because + * it is not known by Hibernate nor JPA and, hence, not picked up from + * their configuration! * * @parameter property="project.build.testOutputDirectory" * @since 1.0.2 */ private String testOutputDirectory; - /** - * Skip execution - *

- * If set to true, the execution is skipped. - *

- * A skipped execution is signaled via the maven-property - * ${hibernate.export.skipped}. - *

- * The execution is skipped automatically, if no modified or newly added - * annotated classes are found and the dialect was not changed. - * - * @parameter property="hibernate.skip" default-value="${maven.test.skip}" - * @since 1.0 - */ - private boolean skip; - /** - * Force execution - *

- * Force execution, even if no modified or newly added annotated classes - * where found and the dialect was not changed. - *

- * skip takes precedence over force. - * - * @parameter property="hibernate.export.force" default-value="false" - * @since 1.0 - */ - private boolean force; + /** Conection parameters *************************************************/ /** * SQL-Driver name. @@ -213,29 +336,8 @@ public abstract class AbstractSchemaMojo extends AbstractMojo */ private String password; - /** - * Hibernate dialect. - * - * @parameter property="hibernate.dialect" - * @since 1.0 - */ - private String dialect; - - /** - * Implicit naming strategy - * - * @parameter property=IMPLICIT_NAMING_STRATEGY - * @since 2.0 - */ - private String implicitNamingStrategy; - /** - * Physical naming strategy - * - * @parameter property=PHYSICAL_NAMING_STRATEGY - * @since 2.0 - */ - private String physicalNamingStrategy; + /** Parameters to locate configuration sources ****************************/ /** * Path to a file or name of a ressource with hibernate properties. @@ -302,8 +404,8 @@ public abstract class AbstractSchemaMojo extends AbstractMojo private String mappings; - @Override - public final void execute() + + public final void execute(String filename) throws MojoFailureException, MojoExecutionException @@ -311,14 +413,14 @@ public abstract class AbstractSchemaMojo extends AbstractMojo if (skip) { getLog().info("Execution of hibernate-maven-plugin was skipped!"); - project.getProperties().setProperty(EXPORT_SKIPPED_PROPERTY, "true"); + project.getProperties().setProperty(SKIPPED, "true"); return; } ModificationTracker tracker; try { - tracker = new ModificationTracker(buildDirectory, getLog()); + tracker = new ModificationTracker(buildDirectory, filename, getLog()); } catch (NoSuchAlgorithmException e) { @@ -353,10 +455,10 @@ public abstract class AbstractSchemaMojo extends AbstractMojo properties.putAll(loadPersistenceUnit(classLoaderService, properties)); /** Overwriting/Completing configuration */ - configure(properties); + configure(properties, tracker); /** Check configuration for modifications */ - if(tracker.check(properties)) + if(tracker.track(properties)) getLog().debug("Configuration has changed."); else getLog().debug("Configuration unchanged."); @@ -384,7 +486,7 @@ public abstract class AbstractSchemaMojo extends AbstractMojo else { getLog().info("Skipping schema generation!"); - project.getProperties().setProperty(EXPORT_SKIPPED_PROPERTY, "true"); + project.getProperties().setProperty(SKIPPED, "true"); return; } } @@ -563,18 +665,50 @@ public abstract class AbstractSchemaMojo extends AbstractMojo } } - private void configure(Properties properties) + private void configure(Properties properties, ModificationTracker tracker) throws MojoFailureException { - /** Overwrite values from properties-file or set, if given */ + /** + * Special treatment for the configuration-value "export": if it is + * switched to "true", the genearation fo the schema should be forced! + */ + if (tracker.check(EXPORT, export.toString()) && export) + tracker.touch(); + + /** + * Configure the generation of the SQL. + * Overwrite values from properties-file if the configuration parameter is + * known to Hibernate. + */ + dialect = configure(properties, dialect, DIALECT); + tracker.track(DELIMITER, delimiter); // << not reflected in hibernate configuration! + format = configure(properties, format, FORMAT_SQL); + createNamespaces = configure(properties, createNamespaces, HBM2DLL_CREATE_NAMESPACES); + implicitNamingStrategy = configure(properties, implicitNamingStrategy, IMPLICIT_NAMING_STRATEGY); + physicalNamingStrategy = configure(properties, physicalNamingStrategy, PHYSICAL_NAMING_STRATEGY); + tracker.track(OUTPUTDIRECTORY, outputDirectory); // << not reflected in hibernate configuration! + tracker.track(SCAN_DEPENDENCIES, scanDependencies); // << not reflected in hibernate configuration! + tracker.track(SCAN_TESTCLASSES, scanTestClasses.toString()); // << not reflected in hibernate configuration! + tracker.track(TEST_OUTPUTDIRECTORY, testOutputDirectory); // << not reflected in hibernate configuration! + + /** + * Special treatment for the configuration-value "show": a change of its + * configured value should not lead to a regeneration of the database + * schama! + */ + if (show == null) + show = Boolean.valueOf(properties.getProperty(SHOW_SQL)); + else + properties.setProperty(SHOW_SQL, show.toString()); - configure(properties, driver, DRIVER, JDBC_DRIVER); - configure(properties, url, URL, JDBC_URL); - configure(properties, username, USER, JDBC_USER); - configure(properties, password, PASS, JDBC_PASSWORD); - configure(properties, dialect, DIALECT); - configure(properties, implicitNamingStrategy, IMPLICIT_NAMING_STRATEGY); - configure(properties, physicalNamingStrategy, PHYSICAL_NAMING_STRATEGY); + /** + * Configure the connection parameters. + * Overwrite values from properties-file. + */ + driver = configure(properties, driver, DRIVER, JDBC_DRIVER); + url = configure(properties, url, URL, JDBC_URL); + username = configure(properties, username, USER, JDBC_USER); + password = configure(properties, password, PASS, JDBC_PASSWORD); if (properties.isEmpty()) { @@ -587,26 +721,30 @@ public abstract class AbstractSchemaMojo extends AbstractMojo getLog().info(" " + entry.getKey() + " = " + entry.getValue()); } - private void configure( + private String configure( Properties properties, String value, String key, String alternativeKey ) { - configure(properties, value, key); - if (properties.containsKey(key) && properties.containsKey(alternativeKey)) + value = configure(properties, value, key); + if (value == null) + return properties.getProperty(alternativeKey); + + if (properties.containsKey(alternativeKey)) { getLog().warn( "Ignoring property " + alternativeKey + "=" + properties.getProperty(alternativeKey) + " in favour for property " + key + "=" + properties.getProperty(key) ); - properties.remove(JDBC_DRIVER); + properties.remove(alternativeKey); } + return properties.getProperty(alternativeKey); } - private void configure(Properties properties, String value, String key) + private String configure(Properties properties, String value, String key) { if (value != null) { @@ -619,6 +757,23 @@ public abstract class AbstractSchemaMojo extends AbstractMojo getLog().debug("Using the value " + value + " for property " + key); properties.setProperty(key, value); } + return properties.getProperty(key); + } + + private boolean configure(Properties properties, Boolean value, String key) + { + if (value != null) + { + if (properties.containsKey(key)) + getLog().debug( + "Overwriting property " + key + "=" + properties.getProperty(key) + + " with the value " + value + ); + else + getLog().debug("Using the value " + value + " for property " + key); + properties.setProperty(key, value.toString()); + } + return Boolean.valueOf(properties.getProperty(key)); } private void addMappings(MetadataSources sources, ModificationTracker tracker) @@ -648,7 +803,7 @@ public abstract class AbstractSchemaMojo extends AbstractMojo if (file.isDirectory()) // TODO: add support to read all mappings under a directory throw new MojoFailureException(file.getAbsolutePath() + " is a directory"); - if (tracker.check(filename, new FileInputStream(file))) + if (tracker.track(filename, new FileInputStream(file))) getLog().debug("Found new or modified mapping-file: " + filename); else getLog().debug("mapping-file unchanged: " + filename); @@ -746,7 +901,7 @@ public abstract class AbstractSchemaMojo extends AbstractMojo } else { - if (tracker.check(packageName, is)) + if (tracker.track(packageName, is)) getLog().debug("New or modified package: " + packageName); else getLog().debug("Unchanged package: " + packageName); @@ -764,7 +919,7 @@ public abstract class AbstractSchemaMojo extends AbstractMojo InputStream is = annotatedClass .getResourceAsStream(resourceName); - if (tracker.check(name, is)) + if (tracker.track(name, is)) getLog().debug("New or modified class: " + name); else getLog().debug("Unchanged class: " + name); diff --git a/src/main/java/de/juplo/plugins/hibernate/CreateMojo.java b/src/main/java/de/juplo/plugins/hibernate/CreateMojo.java index 19078ea8..a89d58d2 100644 --- a/src/main/java/de/juplo/plugins/hibernate/CreateMojo.java +++ b/src/main/java/de/juplo/plugins/hibernate/CreateMojo.java @@ -34,49 +34,27 @@ import org.hibernate.tool.hbm2ddl.SchemaExport; */ public class CreateMojo extends AbstractSchemaMojo { - /** - * Export the database-schma to the database. - * If set to false, only the SQL-script is created and the - * database is not touched. - * - * @parameter property="hibernate.export.export" default-value="true" - * @since 2.0 - */ - private boolean export; - - /** - * Create the catalog - * If set to false, only the SQL-script is created and the - * database is not touched. - * - * @parameter property=org.hibernate.cfg.AvailableSettings.HBM2DDL_IMPORT_FILES_SQL_EXTRACTOR default-value="false" - * @since 2.0 - */ - private boolean createNamespaces; // TODO handle in configure-Method - /** * Output file. + *

+ * If the specified filename is not absolut, the file will be created + * relative to the project build directory + * (project.build.directory). * - * @parameter property="hibernate.export.schema.filename" default-value="${project.build.directory}/schema.sql" + * @parameter property="hibernate.schema.export.create" default-value="schema.sql" * @since 1.0 */ private String outputFile; - /** - * Delimiter in output-file. - * - * @parameter property="hibernate.export.schema.delimiter" default-value=";" - * @since 1.0 - */ - private String delimiter; - /** - * Format output-file. - * - * @parameter property="hibernate.export.schema.format" default-value="true" - * @since 1.0 - */ - private boolean format; + @Override + public final void execute() + throws + MojoFailureException, + MojoExecutionException + { + super.execute(outputFile); + } @Override diff --git a/src/main/java/de/juplo/plugins/hibernate/ModificationTracker.java b/src/main/java/de/juplo/plugins/hibernate/ModificationTracker.java index e03f78e9..02820693 100644 --- a/src/main/java/de/juplo/plugins/hibernate/ModificationTracker.java +++ b/src/main/java/de/juplo/plugins/hibernate/ModificationTracker.java @@ -26,8 +26,6 @@ import org.apache.maven.plugin.logging.Log; */ public class ModificationTracker { - public final static String MD5S = "hibernate-generatedschema.md5s"; - private Map properties; private Map classes; @@ -41,13 +39,23 @@ public class ModificationTracker private final Log log; - ModificationTracker(String buildDirectory, Log log) + ModificationTracker(String buildDirectory, String filename, Log log) throws NoSuchAlgorithmException { propertyNames = new HashSet(); classNames = new HashSet(); - saved = new File(buildDirectory + File.separator + MD5S); + File output = new File(filename + ".md5s"); + if (output.isAbsolute()) + { + saved = output; + } + else + { + // Interpret relative file path relative to build directory + saved = new File(buildDirectory, output.getPath()); + log.debug("Adjusted relative path, resulting path is " + saved.getPath()); + } digest = java.security.MessageDigest.getInstance("MD5"); this.log = log; } @@ -79,7 +87,7 @@ public class ModificationTracker } - boolean check(String name, InputStream is) throws IOException + boolean track(String name, InputStream is) throws IOException { boolean result = check(classes, name, calculate(is)); classNames.add(name); @@ -87,23 +95,34 @@ public class ModificationTracker return result; } + boolean check(String name, String property) { - boolean result = check(properties, name, property); propertyNames.add(name); + return check(properties, name, property); + } + + boolean track(String name, String property) + { + boolean result = check(name, property); modified |= result; return result; } - boolean check(Properties properties) + boolean track(Properties properties) { boolean result = false; for (String name : properties.stringPropertyNames()) - result |= check(name, properties.getProperty(name)); + result |= track(name, properties.getProperty(name)); return result; } + void touch() + { + modified = true; + } + boolean modified() { modified |= !propertyNames.containsAll(properties.keySet());