diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/BaseQueryValidator.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/BaseQueryValidator.java index b6a4f200b0..3c21ba8d80 100644 --- a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/BaseQueryValidator.java +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/BaseQueryValidator.java @@ -25,36 +25,51 @@ import org.apache.cayenne.util.Util; import org.apache.cayenne.validation.ValidationResult; +import java.util.function.Supplier; /** * Base validation for all query types */ -class BaseQueryValidator extends ConfigurationNodeValidator { +abstract class BaseQueryValidator extends ConfigurationNodeValidator { - void validateCacheGroup(QueryDescriptor query, ValidationResult validationResult) { - String cacheGroup = query.getProperty(QueryMetadata.CACHE_GROUPS_PROPERTY); - if(cacheGroup != null && cacheGroup.contains(",")) { - addFailure(validationResult, query, "Invalid cache group \"%s\", " + - "multiple groups are deprecated", cacheGroup); - } + /** + * @param configSupplier the config defining the behavior of this validator. + * @since 5.0 + */ + public BaseQueryValidator(Supplier configSupplier) { + super(configSupplier); + } + + @Override + public void validate(T node, ValidationResult validationResult) { + validateQuery(node, validationResult); + } + + protected Performer validateQuery(T query, ValidationResult validationResult) { + return on(query, validationResult) + .performIfEnabled(Inspection.QUERY_NO_NAME, this::checkForName) + .performIfEnabled(Inspection.QUERY_NAME_DUPLICATE, this::checkForNameDuplicates) + .performIfEnabled(Inspection.QUERY_MULTI_CACHE_GROUP, this::checkForMultiCacheGroup); } - void validateName(QueryDescriptor query, ValidationResult validationResult) { - final String name = query.getName(); + void checkForName(T query, ValidationResult validationResult) { // Must have name + String name = query.getName(); if (Util.isEmptyString(name)) { addFailure(validationResult, query, "Unnamed " + query.getType()); - return; } + } + void checkForNameDuplicates(T query, ValidationResult validationResult) { + String name = query.getName(); DataMap map = query.getDataMap(); - if (map == null) { + if (map == null || Util.isEmptyString(name)) { return; } // check for duplicate names in the parent context - if(hasDuplicateQueryDescriptorInDataMap(query, map)) { + if (hasDuplicateQueryDescriptorInDataMap(query, map)) { addFailure(validationResult, query, "Duplicate query name: %s", name); return; } @@ -71,14 +86,21 @@ void validateName(QueryDescriptor query, ValidationResult validationResult) { } if (hasDuplicateQueryDescriptorInDataMap(query, nextMap)) { - addFailure(validationResult, query, - "Duplicate %s name in another DataMap: %s", + addFailure(validationResult, query, "Duplicate %s name in another DataMap: %s", query.getType(), name); return; } } } + void checkForMultiCacheGroup(T query, ValidationResult validationResult) { + String cacheGroup = query.getProperty(QueryMetadata.CACHE_GROUPS_PROPERTY); + if (cacheGroup != null && cacheGroup.contains(",")) { + addFailure(validationResult, query, "Invalid cache group '%s', multiple groups are deprecated", + cacheGroup); + } + } + private boolean hasDuplicateQueryDescriptorInDataMap(QueryDescriptor queryDescriptor, DataMap dataMap) { for (final QueryDescriptor otherQuery : dataMap.getQueryDescriptors()) { if (otherQuery == queryDescriptor) { diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ConfigurationNodeValidator.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ConfigurationNodeValidator.java index c05bf0d989..99fbe728a6 100644 --- a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ConfigurationNodeValidator.java +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ConfigurationNodeValidator.java @@ -18,29 +18,82 @@ ****************************************************************/ package org.apache.cayenne.project.validation; +import org.apache.cayenne.configuration.ConfigurationNode; import org.apache.cayenne.validation.SimpleValidationFailure; +import org.apache.cayenne.validation.ValidationFailure; import org.apache.cayenne.validation.ValidationResult; +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Predicate; +import java.util.function.Supplier; + /** * A base superclass of various node validators. - * + * * @since 3.1 */ -public abstract class ConfigurationNodeValidator { +public abstract class ConfigurationNodeValidator { + + protected final Supplier configSupplier; + + /** + * @param configSupplier the config defining the behavior of this validator. + * @since 5.0 + */ + public ConfigurationNodeValidator(Supplier configSupplier) { + this.configSupplier = configSupplier; + } - public void addFailure( - ValidationResult validationResult, - Object source, - String messageFormat, - Object... messageParameters) { + /** + * @param node the node that needs to be validated. + * @param validationResult the appendable validation result. + * @since 5.0 + */ + public abstract void validate(T node, ValidationResult validationResult); + public void addFailure(ValidationResult validationResult, T source, String messageFormat, + Object... messageParameters) { String message = String.format(messageFormat, messageParameters); - validationResult.addFailure(new SimpleValidationFailure(source, message)); + validationResult.addFailure(new ProjectValidationFailure(source, message)); + } + + public void addFailure(ValidationResult validationResult, SimpleValidationFailure failure) { + validationResult.addFailure(failure); } - - public void addFailure( - ValidationResult validationResult, - SimpleValidationFailure failure) { - validationResult.addFailure(failure); + + protected Performer on(T node, ValidationResult validationResult) { + return new Performer<>(node, validationResult); + } + + protected class Performer { + + private final N node; + private final ValidationResult validationResult; + + protected Performer(N node, ValidationResult validationResult) { + this.node = node; + this.validationResult = validationResult; + } + + protected Performer performIfEnabled(Inspection inspection, BiConsumer action) { + return performIfEnabled(inspection, () -> action.accept(node, validationResult)); + } + + protected Performer performIfEnabled(Inspection inspection, Runnable action) { + if (configSupplier.get().isEnabled(inspection)) { + performAndMarkFailures(inspection, action); + } + return this; + } + + private void performAndMarkFailures(Inspection inspection, Runnable action) { + List failuresBefore = new ArrayList<>(validationResult.getFailures()); + action.run(); + validationResult.getFailures().stream() + .filter(Predicate.not(failuresBefore::contains)) + .forEach(failure -> ((ProjectValidationFailure) failure).setInspection(inspection)); + } } } diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DataChannelValidator.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DataChannelValidator.java index 110eb2ec7c..9ddc33aac6 100644 --- a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DataChannelValidator.java +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DataChannelValidator.java @@ -22,10 +22,25 @@ import org.apache.cayenne.util.Util; import org.apache.cayenne.validation.ValidationResult; -class DataChannelValidator extends ConfigurationNodeValidator { +import java.util.function.Supplier; - void validate(DataChannelDescriptor domain, ValidationResult validationResult) { +class DataChannelValidator extends ConfigurationNodeValidator { + /** + * @param configSupplier the config defining the behavior of this validator. + * @since 5.0 + */ + public DataChannelValidator(Supplier configSupplier) { + super(configSupplier); + } + + @Override + public void validate(DataChannelDescriptor node, ValidationResult validationResult) { + on(node, validationResult) + .performIfEnabled(Inspection.DATA_CHANNEL_NO_NAME, this::checkForName); + } + + private void checkForName(DataChannelDescriptor domain, ValidationResult validationResult) { String name = domain.getName(); if (Util.isEmptyString(name)) { addFailure(validationResult, domain, "Unnamed DataDomain"); diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DataMapValidator.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DataMapValidator.java index bce8b57b6b..bc78578a29 100644 --- a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DataMapValidator.java +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DataMapValidator.java @@ -24,45 +24,38 @@ import org.apache.cayenne.util.Util; import org.apache.cayenne.validation.ValidationResult; -class DataMapValidator extends ConfigurationNodeValidator { +import java.util.function.Supplier; - void validate(DataMap map, ValidationResult validationResult) { - validateName(map, validationResult); - validateNodeLinks(map, validationResult); - validateJavaPackage(map, validationResult); - } - - private void validateNodeLinks(DataMap map, ValidationResult validationResult) { - DataChannelDescriptor domain = map.getDataChannelDescriptor(); - if (domain == null) { - return; - } +class DataMapValidator extends ConfigurationNodeValidator { - boolean unlinked = true; - int nodeCount = 0; - for (DataNodeDescriptor node : domain.getNodeDescriptors()) { - nodeCount++; - if (node.getDataMapNames().contains(map.getName())) { - unlinked = false; - break; - } - } + /** + * @param configSupplier the config defining the behavior of this validator. + * @since 5.0 + */ + public DataMapValidator(Supplier configSupplier) { + super(configSupplier); + } - if (unlinked && nodeCount > 0) { - addFailure(validationResult, map, "DataMap is not linked to any DataNodes"); - } + @Override + public void validate(DataMap node, ValidationResult validationResult) { + on(node, validationResult) + .performIfEnabled(Inspection.DATA_MAP_NO_NAME, this::checkForName) + .performIfEnabled(Inspection.DATA_MAP_NAME_DUPLICATE, this::checkForNameDuplicates) + .performIfEnabled(Inspection.DATA_MAP_NODE_LINKAGE, this::checkForNodeLinkage) + .performIfEnabled(Inspection.DATA_MAP_JAVA_PACKAGE, this::validateJavaPackage); } - private void validateName(DataMap map, ValidationResult validationResult) { + private void checkForName(DataMap map, ValidationResult validationResult) { String name = map.getName(); - if (Util.isEmptyString(name)) { addFailure(validationResult, map, "Unnamed DataMap"); - return; } + } + private void checkForNameDuplicates(DataMap map, ValidationResult validationResult) { + String name = map.getName(); DataChannelDescriptor domain = map.getDataChannelDescriptor(); - if (domain == null) { + if (domain == null || Util.isEmptyString(name)) { return; } @@ -71,7 +64,6 @@ private void validateName(DataMap map, ValidationResult validationResult) { if (otherMap == map) { continue; } - if (name.equals(otherMap.getName())) { addFailure(validationResult, map, "Duplicate DataMap name: %s", name); return; @@ -79,17 +71,37 @@ private void validateName(DataMap map, ValidationResult validationResult) { } } + private void checkForNodeLinkage(DataMap map, ValidationResult validationResult) { + DataChannelDescriptor domain = map.getDataChannelDescriptor(); + if (domain == null) { + return; + } + + boolean linked = false; + int nodeCount = 0; + for (DataNodeDescriptor node : domain.getNodeDescriptors()) { + nodeCount++; + if (node.getDataMapNames().contains(map.getName())) { + linked = true; + break; + } + } + + if (!linked && nodeCount > 0) { + addFailure(validationResult, map, "DataMap is not linked to any DataNodes"); + } + } + private void validateJavaPackage(DataMap map, ValidationResult validationResult) { String javaPackage = map.getDefaultPackage(); - - if(Util.isEmptyString(javaPackage)) { + if (Util.isEmptyString(javaPackage)) { addFailure(validationResult, map, "Java package is not set in DataMap '%s'", map.getName()); return; } NameValidationHelper helper = NameValidationHelper.getInstance(); String invalidChars = helper.invalidCharsInJavaClassName(javaPackage); - if(invalidChars != null) { + if (invalidChars != null) { addFailure(validationResult, map, "DataMap '%s' Java package '%s' contains invalid characters: %s", map.getName(), javaPackage, invalidChars); } diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DataNodeValidator.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DataNodeValidator.java index a8db9d21ed..b6158c1fab 100644 --- a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DataNodeValidator.java +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DataNodeValidator.java @@ -24,40 +24,39 @@ import org.apache.cayenne.util.Util; import org.apache.cayenne.validation.ValidationResult; -class DataNodeValidator extends ConfigurationNodeValidator { +import java.util.function.Supplier; - void validate(DataNodeDescriptor node, ValidationResult validationResult) { - validateName(node, validationResult); - validateConnection(node, validationResult); - } - - void validateConnection(DataNodeDescriptor node, ValidationResult validationResult) { +class DataNodeValidator extends ConfigurationNodeValidator { - String factory = node.getDataSourceFactoryType(); + /** + * @param configSupplier the config defining the behavior of this validator. + * @since 5.0 + */ + public DataNodeValidator(Supplier configSupplier) { + super(configSupplier); + } - // TODO: andrus 03/10/2010 - null factory is allowed, however - // 'getDataSourceDescriptor' must ne not null in this case + @Override + public void validate(DataNodeDescriptor node, ValidationResult validationResult) { + on(node, validationResult) + .performIfEnabled(Inspection.DATA_NODE_NO_NAME, this::checkForName) + .performIfEnabled(Inspection.DATA_NODE_NAME_DUPLICATE, this::checkForNameDuplicates) + .performIfEnabled(Inspection.DATA_NODE_CONNECTION_PARAMS, this::validateConnection); + } - if (factory != null - && !XMLPoolingDataSourceFactory.class.getName().equals(factory)) { - String parameters = node.getParameters(); - if (Util.isEmptyString(parameters)) { - addFailure( - validationResult, - node, - "DataNode has empty 'parameters' string"); - } + private void checkForName(DataNodeDescriptor node, ValidationResult validationResult) { + String name = node.getName(); + if (Util.isEmptyString(name)) { + addFailure(validationResult, node, "Unnamed DataNode"); } + } - void validateName(DataNodeDescriptor node, ValidationResult validationResult) { + private void checkForNameDuplicates(DataNodeDescriptor node, ValidationResult validationResult) { String name = node.getName(); - if (Util.isEmptyString(name)) { - addFailure(validationResult, node, "Unnamed DataNode"); return; } - DataChannelDescriptor dataChannelDescriptor = node.getDataChannelDescriptor(); // check for duplicate names in the parent context @@ -68,8 +67,23 @@ void validateName(DataNodeDescriptor node, ValidationResult validationResult) { if (name.equals(otherNode.getName())) { addFailure(validationResult, node, "Duplicate DataNode name: %s", name); - break; + return; } } } + + private void validateConnection(DataNodeDescriptor node, ValidationResult validationResult) { + String factory = node.getDataSourceFactoryType(); + + // TODO: andrus 03/10/2010 - null factory is allowed, however + // 'getDataSourceDescriptor' must ne not null in this case + + if (factory == null || XMLPoolingDataSourceFactory.class.getName().equals(factory)) { + return; + } + String parameters = node.getParameters(); + if (Util.isEmptyString(parameters)) { + addFailure(validationResult, node, "DataNode has empty 'parameters' string"); + } + } } diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DbAttributeValidator.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DbAttributeValidator.java index 3dab6c7ec7..f391c951ee 100644 --- a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DbAttributeValidator.java +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DbAttributeValidator.java @@ -23,42 +23,65 @@ import org.apache.cayenne.util.Util; import org.apache.cayenne.validation.ValidationResult; -class DbAttributeValidator extends ConfigurationNodeValidator { +import java.util.function.Supplier; - void validate(DbAttribute attribute, ValidationResult validationResult) { +class DbAttributeValidator extends ConfigurationNodeValidator { + + /** + * @param configSupplier the config defining the behavior of this validator. + * @since 5.0 + */ + public DbAttributeValidator(Supplier configSupplier) { + super(configSupplier); + } + + @Override + public void validate(DbAttribute node, ValidationResult validationResult) { + on(node, validationResult) + .performIfEnabled(Inspection.DB_ATTRIBUTE_NO_NAME, this::checkForName) + .performIfEnabled(Inspection.DB_ATTRIBUTE_INVALID_NAME, this::validateName) + .performIfEnabled(Inspection.DB_ATTRIBUTE_NO_TYPE, this::checkForType) + .performIfEnabled(Inspection.DB_ATTRIBUTE_NO_LENGTH, this::checkForLength); + } + + private void checkForName(DbAttribute attribute, ValidationResult validationResult) { // Must have name if (Util.isEmptyString(attribute.getName())) { addFailure(validationResult, attribute, "Unnamed DbAttribute"); - } else { - NameValidationHelper helper = NameValidationHelper.getInstance(); - String invalidChars = helper.invalidCharsInDbPathComponent(attribute - .getName()); + } + } - if (invalidChars != null) { - addFailure( - validationResult, - attribute, - "DbAttribute name '%s' contains invalid characters: %s", - attribute.getName(), - invalidChars); - } + private void validateName(DbAttribute attribute, ValidationResult validationResult) { + NameValidationHelper helper = NameValidationHelper.getInstance(); + String name = attribute.getName(); + String invalidChars = helper.invalidCharsInDbPathComponent(name); + if (Util.isEmptyString(name)) { + return; } + if (invalidChars != null) { + addFailure(validationResult, attribute, "DbAttribute name '%s' contains invalid characters: %s", + name, invalidChars); + } + } + + private void checkForType(DbAttribute attribute, ValidationResult validationResult) { // all attributes must have type if (attribute.getType() == TypesMapping.NOT_DEFINED) { addFailure(validationResult, attribute, "DbAttribute has no type"); - } else if (attribute.getMaxLength() < 0 + } + } + + private void checkForLength(DbAttribute attribute, ValidationResult validationResult) { + if (attribute.getMaxLength() < 0 && (attribute.getType() == java.sql.Types.VARCHAR - || attribute.getType() == java.sql.Types.NVARCHAR - || attribute.getType() == java.sql.Types.CHAR - || attribute.getType() == java.sql.Types.NCHAR)) { + || attribute.getType() == java.sql.Types.NVARCHAR + || attribute.getType() == java.sql.Types.CHAR + || attribute.getType() == java.sql.Types.NCHAR)) { // VARCHAR and CHAR attributes must have max length - addFailure( - validationResult, - attribute, - "Character DbAttribute '%s' doesn't have max length", + addFailure(validationResult, attribute, "Character DbAttribute '%s' doesn't have max length", attribute.getName()); } } diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DbEntityValidator.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DbEntityValidator.java index f06f9ec378..e891341803 100644 --- a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DbEntityValidator.java +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DbEntityValidator.java @@ -23,57 +23,40 @@ import org.apache.cayenne.util.Util; import org.apache.cayenne.validation.ValidationResult; -class DbEntityValidator extends ConfigurationNodeValidator { +import java.util.function.Supplier; - void validate(DbEntity entity, ValidationResult validationResult) { - validateName(entity, validationResult); - validateAttributes(entity, validationResult); - validatePK(entity, validationResult); - } +class DbEntityValidator extends ConfigurationNodeValidator { /** - * Validates the presence of the primary key. A warning is given only if the parent - * map also contains an ObjEntity mapped to this entity, since unmapped primary key is - * ok if working with data rows. + * @param configSupplier the config defining the behavior of this validator. + * @since 5.0 */ - void validatePK(DbEntity entity, ValidationResult validationResult) { - if (entity.getAttributes().size() > 0 && entity.getPrimaryKeys().size() == 0) { - DataMap map = entity.getDataMap(); - if (map != null && map.getMappedEntities(entity).size() > 0) { - - addFailure( - validationResult, - entity, - "DbEntity '%s' has no primary key attributes defined", - entity.getName()); - } - } + public DbEntityValidator(Supplier configSupplier) { + super(configSupplier); } - /** - * Tables must have columns. - */ - void validateAttributes(DbEntity entity, ValidationResult validationResult) { - if (entity.getAttributes().size() == 0) { - addFailure( - validationResult, - entity, - "DbEntity '%s' has no attributes defined", - entity.getName()); - } + @Override + public void validate(DbEntity node, ValidationResult validationResult) { + on(node, validationResult) + .performIfEnabled(Inspection.DB_ENTITY_NO_NAME, this::checkForName) + .performIfEnabled(Inspection.DB_ENTITY_NAME_DUPLICATE, this::checkForNameDuplicates) + .performIfEnabled(Inspection.DB_ENTITY_NO_ATTRIBUTES, this::checkForAttributes) + .performIfEnabled(Inspection.DB_ENTITY_NO_PK, this::checkForPK); } - void validateName(DbEntity entity, ValidationResult validationResult) { - String name = entity.getName(); + private void checkForName(DbEntity entity, ValidationResult validationResult) { // Must have name + String name = entity.getName(); if (Util.isEmptyString(name)) { addFailure(validationResult, entity, "Unnamed DbEntity"); - return; } + } + private void checkForNameDuplicates(DbEntity entity, ValidationResult validationResult) { + String name = entity.getName(); DataMap map = entity.getDataMap(); - if (map == null) { + if (map == null || Util.isEmptyString(name)) { return; } @@ -89,4 +72,29 @@ void validateName(DbEntity entity, ValidationResult validationResult) { } } } + + /** + * Tables must have columns. + */ + private void checkForAttributes(DbEntity entity, ValidationResult validationResult) { + if (entity.getAttributes().isEmpty()) { + addFailure(validationResult, entity, "DbEntity '%s' has no attributes defined", entity.getName()); + } + } + + /** + * Validates the presence of the primary key. A warning is given only if the parent + * map also contains an ObjEntity mapped to this entity, since unmapped primary key is + * ok if working with data rows. + */ + private void checkForPK(DbEntity entity, ValidationResult validationResult) { + if (entity.getAttributes().isEmpty() || !entity.getPrimaryKeys().isEmpty()) { + return; + } + DataMap map = entity.getDataMap(); + if (map == null || map.getMappedEntities(entity).isEmpty()) { + return; + } + addFailure(validationResult, entity, "DbEntity '%s' has no primary key attributes defined", entity.getName()); + } } diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DbRelationshipValidator.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DbRelationshipValidator.java index 9fffbbc0f3..287546d0f7 100644 --- a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DbRelationshipValidator.java +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DbRelationshipValidator.java @@ -29,107 +29,140 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.function.Supplier; -class DbRelationshipValidator extends ConfigurationNodeValidator { +class DbRelationshipValidator extends ConfigurationNodeValidator { - void validate(DbRelationship relationship, ValidationResult validationResult) { + /** + * @param configSupplier the config defining the behavior of this validator. + * @since 5.0 + */ + public DbRelationshipValidator(Supplier configSupplier) { + super(configSupplier); + } - if (relationship.getTargetEntity() == null) { - addFailure( - validationResult, - relationship, - "DbRelationship '%s' has no target entity", - toString(relationship)); - } else if (relationship.getJoins().isEmpty()) { - addFailure( - validationResult, - relationship, - "DbRelationship '%s' has no joins", + @Override + public void validate(DbRelationship node, ValidationResult validationResult) { + on(node, validationResult) + .performIfEnabled(Inspection.DB_RELATIONSHIP_NO_NAME, this::checkForName) + .performIfEnabled(Inspection.DB_RELATIONSHIP_NAME_DUPLICATE, this::checkForNameDuplicates) + .performIfEnabled(Inspection.DB_RELATIONSHIP_INVALID_NAME, this::validateName) + .performIfEnabled(Inspection.DB_RELATIONSHIP_PATH_DUPLICATE, this::checkForPathDuplicates) + .performIfEnabled(Inspection.DB_RELATIONSHIP_NO_TARGET, this::checkForTarget) + .performIfEnabled(Inspection.DB_RELATIONSHIP_TARGET_NOT_PK, this::checkForTargetPK) + .performIfEnabled(Inspection.DB_RELATIONSHIP_NO_JOINS, this::checkForJoins) + .performIfEnabled(Inspection.DB_RELATIONSHIP_INVALID_JOIN, this::validateJoins) + .performIfEnabled(Inspection.DB_RELATIONSHIP_BOTH_TO_MANY, this::validateReverse) + .performIfEnabled(Inspection.DB_RELATIONSHIP_DIFFERENT_TYPES, this::checkForSameTypes) + .performIfEnabled(Inspection.DB_RELATIONSHIP_GENERATED_WITH_DEPENDENT_PK, + this::checkOnGeneratedStrategyConflict); + } + + private void checkForName(DbRelationship relationship, ValidationResult validationResult) { + if (Util.isEmptyString(relationship.getName())) { + addFailure(validationResult, relationship, "Unnamed DbRelationship"); + } + } + + private void checkForNameDuplicates(DbRelationship relationship, ValidationResult validationResult) { + if (relationship.getSourceEntity().getAttribute(relationship.getName()) != null) { + // check if there are attributes having the same name + addFailure(validationResult, relationship, + "Name of DbRelationship '%s' conflicts with the name of one of DbAttributes in the same entity", toString(relationship)); - } else { - // validate joins - for (DbJoin join : relationship.getJoins()) { - if (join.getSource() == null && join.getTarget() == null) { - addFailure( - validationResult, - relationship, - "DbRelationship '%s' has a join with no source and target attributes selected", - toString(relationship)); - } else if (join.getSource() == null) { - addFailure( - validationResult, - relationship, - "DbRelationship '%s' has a join with no source attribute selected", - toString(relationship)); - } else if (join.getTarget() == null) { - addFailure( - validationResult, - relationship, - "DbRelationship '%s' has a join with no target attribute selected", - toString(relationship)); - } + } + } + + private void validateName(DbRelationship relationship, ValidationResult validationResult) { + NameValidationHelper helper = NameValidationHelper.getInstance(); + String invalidChars = helper.invalidCharsInDbPathComponent(relationship.getName()); + if (invalidChars != null) { + addFailure(validationResult, relationship, "Name of DbRelationship '%s' contains invalid characters: %s", + toString(relationship), invalidChars); + } + } + + /** + * Per CAY-1813, make sure two (or more) DbRelationships do not map to the + * same database path. + */ + private void checkForPathDuplicates(DbRelationship relationship, ValidationResult validationResult) { + if (relationship == null || relationship.getName() == null || relationship.getTargetEntityName() == null) { + return; + } + String dbRelationshipPath = relationship.getTargetEntityName() + "." + getJoins(relationship); + DbEntity entity = relationship.getSourceEntity(); + + for (DbRelationship otherRelationship : entity.getRelationships()) { + if (relationship == otherRelationship) { + continue; } + String otherDbRelationshipPath = otherRelationship.getTargetEntityName() + "." + getJoins(otherRelationship); + if (dbRelationshipPath.equals(otherDbRelationshipPath)) { + addFailure(validationResult, relationship, + "DbEntity '%s' contains a duplicate DbRelationship mapping ('%s' -> '%s')", + entity.getName(), relationship.getName(), dbRelationshipPath); + return; + } + } + } + + private void checkForTarget(DbRelationship relationship, ValidationResult validationResult) { + if (relationship.getTargetEntity() == null) { + addFailure(validationResult, relationship, "DbRelationship '%s' has no target entity", + toString(relationship)); } + } - if(!relationship.isToPK()) { + private void checkForTargetPK(DbRelationship relationship, ValidationResult validationResult) { + if (!relationship.isToPK()) { DbRelationship reverseRelationship = relationship.getReverseRelationship(); - if(reverseRelationship != null && !reverseRelationship.isToPK()) { - addFailure( - validationResult, - relationship, + if (reverseRelationship != null && !reverseRelationship.isToPK()) { + addFailure(validationResult, relationship, "DbRelationship '%s' has join not to PK. Cayenne doesn't allow this type of relationship", toString(relationship)); } } + } - if (Util.isEmptyString(relationship.getName())) { - addFailure(validationResult, relationship, "Unnamed DbRelationship"); - } else if (relationship.getSourceEntity().getAttribute(relationship.getName()) != null) { - // check if there are attributes having the same name - addFailure( - validationResult, - relationship, - "Name of DbRelationship '%s' conflicts with the name of one of DbAttributes in the same entity", - toString(relationship)); - } else { - NameValidationHelper helper = NameValidationHelper.getInstance(); - String invalidChars = helper.invalidCharsInDbPathComponent(relationship.getName()); - if (invalidChars != null) { - addFailure( - validationResult, - relationship, - "Name of DbRelationship '%s' contains invalid characters: %s", - toString(relationship), - invalidChars); - } + private void checkForJoins(DbRelationship relationship, ValidationResult validationResult) { + if (relationship.getJoins().isEmpty()) { + addFailure(validationResult, relationship, "DbRelationship '%s' has no joins", toString(relationship)); } - - checkForDuplicates(relationship, validationResult); - checkOnGeneratedStrategyConflict(relationship, validationResult); - checkToMany(relationship, validationResult); } - private void checkToMany(DbRelationship relationship, ValidationResult validationResult) { - if(relationship != null) { - if(relationship.getReverseRelationship() != null - && relationship.isToMany() && relationship.getReverseRelationship().isToMany()) { - addFailure( - validationResult, - relationship, - "Relationship '%s' and reverse '%s' are both toMany", - relationship.getName(), relationship.getReverseRelationship().getName()); + private void validateJoins(DbRelationship relationship, ValidationResult validationResult) { + for (DbJoin join : relationship.getJoins()) { + if (join.getSource() == null && join.getTarget() == null) { + addFailure(validationResult, relationship, + "DbRelationship '%s' has a join with no source and target attributes selected", + toString(relationship)); + } else if (join.getSource() == null) { + addFailure(validationResult, relationship, + "DbRelationship '%s' has a join with no source attribute selected", + toString(relationship)); + } else if (join.getTarget() == null) { + addFailure(validationResult, relationship, + "DbRelationship '%s' has a join with no target attribute selected", + toString(relationship)); } - checkTypesOfAttributesInRelationship(relationship, validationResult); } } - private void checkTypesOfAttributesInRelationship(DbRelationship relationship, ValidationResult validationResult) { - for (DbJoin join: relationship.getJoins()) { + private void validateReverse(DbRelationship relationship, ValidationResult validationResult) { + if (relationship.getReverseRelationship() != null + && relationship.isToMany() + && relationship.getReverseRelationship().isToMany()) { + addFailure(validationResult, relationship, "Relationship '%s' and reverse '%s' are both toMany", + relationship.getName(), relationship.getReverseRelationship().getName()); + } + } + + private void checkForSameTypes(DbRelationship relationship, ValidationResult validationResult) { + for (DbJoin join : relationship.getJoins()) { if (join.getSource() != null && join.getTarget() != null && join.getSource().getType() != join.getTarget().getType()) { - addFailure( - validationResult, - relationship, + addFailure(validationResult, relationship, "Attributes '%s' and '%s' have different types in a relationship '%s'", join.getSourceName(), join.getTargetName(), relationship.getName()); } @@ -137,53 +170,15 @@ private void checkTypesOfAttributesInRelationship(DbRelationship relationship, V } private void checkOnGeneratedStrategyConflict(DbRelationship relationship, ValidationResult validationResult) { - if (relationship.isToDependentPK()) { - Collection attributes = relationship.getTargetEntity().getGeneratedAttributes(); - for (DbAttribute attribute : attributes) { - if (attribute.isGenerated()) { - addFailure( - validationResult, - relationship, - "'To Dep Pk' incompatible with Database-Generated on '%s' relationship", - toString(relationship)); - } - } + if (!relationship.isToDependentPK()) { + return; } - } - - /** - * Per CAY-1813, make sure two (or more) DbRelationships do not map to the - * same database path. - */ - private void checkForDuplicates(DbRelationship relationship, ValidationResult validationResult) { - if (relationship != null && - relationship.getName() != null && - relationship.getTargetEntityName() != null) { - - String dbRelationshipPath = - relationship.getTargetEntityName() + - "." + - getJoins(relationship); - - DbEntity entity = relationship.getSourceEntity(); - - for (DbRelationship comparisonRelationship : entity.getRelationships()) { - if (relationship != comparisonRelationship) { - String comparisonDbRelationshipPath = - comparisonRelationship.getTargetEntityName() + - "." + - getJoins(comparisonRelationship); - - if (dbRelationshipPath.equals(comparisonDbRelationshipPath)) { - addFailure(validationResult, - relationship, - "DbEntity '%s' contains a duplicate DbRelationship mapping ('%s' -> '%s')", - entity.getName(), - relationship.getName(), - dbRelationshipPath); - return; // Duplicate found, stop. - } - } + Collection attributes = relationship.getTargetEntity().getGeneratedAttributes(); + for (DbAttribute attribute : attributes) { + if (attribute.isGenerated()) { + addFailure(validationResult, relationship, + "'To Dep Pk' incompatible with Database-Generated on '%s' relationship", + toString(relationship)); } } } @@ -205,5 +200,4 @@ private String toString(DbRelationship relationship) { return relationship.getSourceEntity().getName() + "." + relationship.getName(); } - } diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DefaultProjectValidator.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DefaultProjectValidator.java index 07ccf31993..be0f7365c8 100644 --- a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DefaultProjectValidator.java +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/DefaultProjectValidator.java @@ -40,63 +40,69 @@ import org.apache.cayenne.map.SelectQueryDescriptor; import org.apache.cayenne.validation.ValidationResult; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + /** * @since 3.1 */ public class DefaultProjectValidator implements ProjectValidator { - private DataChannelValidator dataChannelValidator; - private DataNodeValidator nodeValidator; - private DataMapValidator mapValidator; - private ObjEntityValidator objEntityValidator; - private ObjAttributeValidator objAttrValidator; - private ObjRelationshipValidator objRelValidator; - private DbEntityValidator dbEntityValidator; - private DbAttributeValidator dbAttrValidator; - private DbRelationshipValidator dbRelValidator; - private EmbeddableAttributeValidator embeddableAttributeValidator; - private EmbeddableValidator embeddableValidator; - private ProcedureValidator procedureValidator; - private ProcedureParameterValidator procedureParameterValidator; - private SelectQueryValidator selectQueryValidator; - private ProcedureQueryValidator procedureQueryValidator; - private EJBQLQueryValidator ejbqlQueryValidator; - private SQLTemplateValidator sqlTemplateValidator; - - DefaultProjectValidator() { - dataChannelValidator = new DataChannelValidator(); - nodeValidator = new DataNodeValidator(); - mapValidator = new DataMapValidator(); - objEntityValidator = new ObjEntityValidator(); - objAttrValidator = new ObjAttributeValidator(); - objRelValidator = new ObjRelationshipValidator(); - dbEntityValidator = new DbEntityValidator(); - dbAttrValidator = new DbAttributeValidator(); - dbRelValidator = new DbRelationshipValidator(); - embeddableAttributeValidator = new EmbeddableAttributeValidator(); - embeddableValidator = new EmbeddableValidator(); - procedureValidator = new ProcedureValidator(); - procedureParameterValidator = new ProcedureParameterValidator(); - selectQueryValidator = new SelectQueryValidator(); - procedureQueryValidator = new ProcedureQueryValidator(); - ejbqlQueryValidator = new EJBQLQueryValidator(); - sqlTemplateValidator = new SQLTemplateValidator(); + protected final Map, ConfigurationNodeValidator> validators; + protected ValidationConfig defaultConfig; + + protected DefaultProjectValidator(Supplier configSupplier) { + validators = prepareValidators(configSupplier); + } + + public DefaultProjectValidator() { + defaultConfig = new ValidationConfig(); + validators = prepareValidators(() -> defaultConfig); } public ValidationResult validate(ConfigurationNode node) { return node.acceptVisitor(new ValidationVisitor()); } + private static Map, ConfigurationNodeValidator> prepareValidators( + Supplier configSupplier) { + Map, ConfigurationNodeValidator> validators = new HashMap<>(); + validators.put(DataChannelDescriptor.class, new DataChannelValidator(configSupplier)); + validators.put(DataNodeDescriptor.class, new DataNodeValidator(configSupplier)); + validators.put(DataMap.class, new DataMapValidator(configSupplier)); + validators.put(ObjEntity.class, new ObjEntityValidator(configSupplier)); + validators.put(ObjAttribute.class, new ObjAttributeValidator(configSupplier)); + validators.put(ObjRelationship.class, new ObjRelationshipValidator(configSupplier)); + validators.put(DbEntity.class, new DbEntityValidator(configSupplier)); + validators.put(DbAttribute.class, new DbAttributeValidator(configSupplier)); + validators.put(DbRelationship.class, new DbRelationshipValidator(configSupplier)); + validators.put(Embeddable.class, new EmbeddableValidator(configSupplier)); + validators.put(EmbeddableAttribute.class, new EmbeddableAttributeValidator(configSupplier)); + validators.put(Procedure.class, new ProcedureValidator(configSupplier)); + validators.put(ProcedureParameter.class, new ProcedureParameterValidator(configSupplier)); + validators.put(SelectQueryDescriptor.class, new SelectQueryValidator(configSupplier)); + validators.put(ProcedureQueryDescriptor.class, new ProcedureQueryValidator(configSupplier)); + validators.put(EJBQLQueryDescriptor.class, new EJBQLQueryValidator(configSupplier)); + validators.put(SQLTemplateDescriptor.class, new SQLTemplateValidator(configSupplier)); + return validators; + } + + @SuppressWarnings("unchecked") + protected ConfigurationNodeValidator getValidator(Class node) { + return (ConfigurationNodeValidator) validators.get(node); + } + class ValidationVisitor implements ConfigurationNodeVisitor { - private ValidationResult validationResult; + private final ValidationResult validationResult; ValidationVisitor() { validationResult = new ValidationResult(); } public ValidationResult visitDataChannelDescriptor(DataChannelDescriptor channelDescriptor) { - dataChannelValidator.validate(channelDescriptor, validationResult); + getValidator(DataChannelDescriptor.class).validate(channelDescriptor, validationResult); for (DataNodeDescriptor node : channelDescriptor.getNodeDescriptors()) { visitDataNodeDescriptor(node); @@ -110,7 +116,7 @@ public ValidationResult visitDataChannelDescriptor(DataChannelDescriptor channel } public ValidationResult visitDataMap(DataMap dataMap) { - mapValidator.validate(dataMap, validationResult); + getValidator(DataMap.class).validate(dataMap, validationResult); for (Embeddable emb : dataMap.getEmbeddables()) { visitEmbeddable(emb); } @@ -135,17 +141,17 @@ public ValidationResult visitDataMap(DataMap dataMap) { } public ValidationResult visitDataNodeDescriptor(DataNodeDescriptor nodeDescriptor) { - nodeValidator.validate(nodeDescriptor, validationResult); + getValidator(DataNodeDescriptor.class).validate(nodeDescriptor, validationResult); return validationResult; } public ValidationResult visitDbAttribute(DbAttribute attribute) { - dbAttrValidator.validate(attribute, validationResult); + getValidator(DbAttribute.class).validate(attribute, validationResult); return validationResult; } public ValidationResult visitDbEntity(DbEntity entity) { - dbEntityValidator.validate(entity, validationResult); + getValidator(DbEntity.class).validate(entity, validationResult); for (DbAttribute attr : entity.getAttributes()) { visitDbAttribute(attr); @@ -158,12 +164,12 @@ public ValidationResult visitDbEntity(DbEntity entity) { } public ValidationResult visitDbRelationship(DbRelationship relationship) { - dbRelValidator.validate(relationship, validationResult); + getValidator(DbRelationship.class).validate(relationship, validationResult); return validationResult; } public ValidationResult visitEmbeddable(Embeddable embeddable) { - embeddableValidator.validate(embeddable, validationResult); + getValidator(Embeddable.class).validate(embeddable, validationResult); for (EmbeddableAttribute attr : embeddable.getAttributes()) { visitEmbeddableAttribute(attr); } @@ -171,17 +177,17 @@ public ValidationResult visitEmbeddable(Embeddable embeddable) { } public ValidationResult visitEmbeddableAttribute(EmbeddableAttribute attribute) { - embeddableAttributeValidator.validate(attribute, validationResult); + getValidator(EmbeddableAttribute.class).validate(attribute, validationResult); return validationResult; } public ValidationResult visitObjAttribute(ObjAttribute attribute) { - objAttrValidator.validate(attribute, validationResult); + getValidator(ObjAttribute.class).validate(attribute, validationResult); return validationResult; } public ValidationResult visitObjEntity(ObjEntity entity) { - objEntityValidator.validate(entity, validationResult); + getValidator(ObjEntity.class).validate(entity, validationResult); for (ObjAttribute attr : entity.getAttributes()) { visitObjAttribute(attr); @@ -194,12 +200,12 @@ public ValidationResult visitObjEntity(ObjEntity entity) { } public ValidationResult visitObjRelationship(ObjRelationship relationship) { - objRelValidator.validate(relationship, validationResult); + getValidator(ObjRelationship.class).validate(relationship, validationResult); return validationResult; } public ValidationResult visitProcedure(Procedure procedure) { - procedureValidator.validate(procedure, validationResult); + getValidator(Procedure.class).validate(procedure, validationResult); ProcedureParameter parameter = procedure.getResultParam(); if (parameter != null) { visitProcedureParameter(parameter); @@ -217,23 +223,23 @@ public ValidationResult visitProcedure(Procedure procedure) { } public ValidationResult visitProcedureParameter(ProcedureParameter parameter) { - procedureParameterValidator.validate(parameter, validationResult); + getValidator(ProcedureParameter.class).validate(parameter, validationResult); return validationResult; } public ValidationResult visitQuery(QueryDescriptor query) { switch (query.getType()) { case QueryDescriptor.SELECT_QUERY: - selectQueryValidator.validate((SelectQueryDescriptor) query, validationResult); + getValidator(SelectQueryDescriptor.class).validate((SelectQueryDescriptor) query, validationResult); break; case QueryDescriptor.SQL_TEMPLATE: - sqlTemplateValidator.validate((SQLTemplateDescriptor) query, validationResult); + getValidator(SQLTemplateDescriptor.class).validate((SQLTemplateDescriptor) query, validationResult); break; case QueryDescriptor.PROCEDURE_QUERY: - procedureQueryValidator.validate((ProcedureQueryDescriptor) query, validationResult); + getValidator(ProcedureQueryDescriptor.class).validate((ProcedureQueryDescriptor) query, validationResult); break; case QueryDescriptor.EJBQL_QUERY: - ejbqlQueryValidator.validate((EJBQLQueryDescriptor) query, validationResult); + getValidator(EJBQLQueryDescriptor.class).validate((EJBQLQueryDescriptor) query, validationResult); break; } diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/EJBQLQueryValidator.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/EJBQLQueryValidator.java index 41760ea986..3a6cb5bb13 100644 --- a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/EJBQLQueryValidator.java +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/EJBQLQueryValidator.java @@ -22,15 +22,29 @@ import org.apache.cayenne.map.EJBQLQueryDescriptor; import org.apache.cayenne.validation.ValidationResult; -class EJBQLQueryValidator extends BaseQueryValidator { +import java.util.function.Supplier; - void validate(EJBQLQueryDescriptor query, ValidationResult validationResult) { +class EJBQLQueryValidator extends BaseQueryValidator { + + /** + * @param configSupplier the config defining the behavior of this validator. + * @since 5.0 + */ + public EJBQLQueryValidator(Supplier configSupplier) { + super(configSupplier); + } + + @Override + protected ConfigurationNodeValidator.Performer validateQuery( + EJBQLQueryDescriptor query, ValidationResult validationResult) { + return super.validateQuery(query, validationResult) + .performIfEnabled(Inspection.EJBQL_QUERY_INVALID_SYNTAX, this::validateSyntax); + } + + private void validateSyntax(EJBQLQueryDescriptor query, ValidationResult validationResult) { PositionException message = new EJBQLStatementValidator().validateEJBQL(query); if (message != null) { addFailure(validationResult, query, "Error in EJBQL query '%s' syntax", query.getName()); } - - validateName(query, validationResult); - validateCacheGroup(query, validationResult); } } diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/EmbeddableAttributeValidator.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/EmbeddableAttributeValidator.java index 11a4a4924e..bb5105aa9a 100644 --- a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/EmbeddableAttributeValidator.java +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/EmbeddableAttributeValidator.java @@ -22,22 +22,38 @@ import org.apache.cayenne.util.Util; import org.apache.cayenne.validation.ValidationResult; -class EmbeddableAttributeValidator extends ConfigurationNodeValidator { +import java.util.function.Supplier; - void validate(EmbeddableAttribute attribute, ValidationResult validationResult) { +class EmbeddableAttributeValidator extends ConfigurationNodeValidator { + + /** + * @param configSupplier the config defining the behavior of this validator. + * @since 5.0 + */ + public EmbeddableAttributeValidator(Supplier configSupplier) { + super(configSupplier); + } + + @Override + public void validate(EmbeddableAttribute node, ValidationResult validationResult) { + on(node, validationResult) + .performIfEnabled(Inspection.EMBEDDABLE_ATTRIBUTE_NO_NAME, this::checkForName) + .performIfEnabled(Inspection.EMBEDDABLE_ATTRIBUTE_NO_TYPE, this::checkForType); + } + + private void checkForName(EmbeddableAttribute attribute, ValidationResult validationResult) { // Must have name if (Util.isEmptyString(attribute.getName())) { addFailure(validationResult, attribute, "Unnamed EmbeddableAttribute"); } + } + + private void checkForType(EmbeddableAttribute attribute, ValidationResult validationResult) { // all attributes must have type if (Util.isEmptyString(attribute.getType())) { - addFailure( - validationResult, - attribute, - "EmbeddableAttribute '%s' has no type", - attribute.getName()); + addFailure(validationResult, attribute, "EmbeddableAttribute '%s' has no type", attribute.getName()); } } } diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/EmbeddableValidator.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/EmbeddableValidator.java index b2cdbdf879..16d01683f1 100644 --- a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/EmbeddableValidator.java +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/EmbeddableValidator.java @@ -24,20 +24,38 @@ import org.apache.cayenne.util.Util; import org.apache.cayenne.validation.ValidationResult; -class EmbeddableValidator extends ConfigurationNodeValidator { +import java.util.function.Supplier; - void validate(Embeddable embeddable, ValidationResult validationResult) { +class EmbeddableValidator extends ConfigurationNodeValidator { + /** + * @param configSupplier the config defining the behavior of this validator. + * @since 5.0 + */ + public EmbeddableValidator(Supplier configSupplier) { + super(configSupplier); + } + + @Override + public void validate(Embeddable node, ValidationResult validationResult) { + on(node, validationResult) + .performIfEnabled(Inspection.EMBEDDABLE_NO_NAME, this::checkForName) + .performIfEnabled(Inspection.EMBEDDABLE_NAME_DUPLICATE, this::checkForNameDuplicates); + } + + private void checkForName(Embeddable embeddable, ValidationResult validationResult) { String name = embeddable.getClassName(); // Must have name if (Util.isEmptyString(name)) { addFailure(validationResult, embeddable, "Unnamed Embeddable"); - return; } + } + private void checkForNameDuplicates(Embeddable embeddable, ValidationResult validationResult) { + String name = embeddable.getClassName(); DataMap map = embeddable.getDataMap(); - if (map == null) { + if (map == null || Util.isEmptyString(name)) { return; } @@ -46,37 +64,28 @@ void validate(Embeddable embeddable, ValidationResult validationResult) { if (otherEmb == embeddable) { continue; } - if (name.equals(otherEmb.getClassName())) { - - addFailure( - validationResult, - embeddable, - "Duplicate Embeddable class name: %s", - name); + addFailure(validationResult, embeddable, "Duplicate Embeddable class name: %s", name); break; } } // check for duplicates in other DataMaps DataChannelDescriptor domain = map.getDataChannelDescriptor(); - if (domain != null) { - for (DataMap nextMap : domain.getDataMaps()) { - if (nextMap == map) { - continue; - } + if (domain == null) { + return; + } + for (DataMap nextMap : domain.getDataMaps()) { + if (nextMap == map) { + continue; + } - // note that lookuo below will return the same embeddable due to the - // shared namespace if not conflicts exist - Embeddable conflictingEmbeddable = nextMap.getEmbeddable(name); - if (conflictingEmbeddable != null && conflictingEmbeddable != embeddable) { - addFailure( - validationResult, - embeddable, - "Duplicate Embeddable name in another DataMap: %s", - name); - break; - } + // note that lookup below will return the same embeddable due to the + // shared namespace if not conflicts exist + Embeddable conflictingEmbeddable = nextMap.getEmbeddable(name); + if (conflictingEmbeddable != null && conflictingEmbeddable != embeddable) { + addFailure(validationResult, embeddable, "Duplicate Embeddable name in another DataMap: %s", name); + break; } } } diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/Inspection.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/Inspection.java new file mode 100644 index 0000000000..136612006e --- /dev/null +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/Inspection.java @@ -0,0 +1,185 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.project.validation; + +/** + * @since 5.0 + */ +// TODO: Some of these probably need a better description. +public enum Inspection { + DATA_CHANNEL_NO_NAME(Group.DATA_CHANNEL, "Empty data domain name"), + + DATA_NODE_NO_NAME(Group.DATA_NODE, "Empty data node name"), + DATA_NODE_NAME_DUPLICATE(Group.DATA_NODE, "Duplicate of a data node name"), + DATA_NODE_CONNECTION_PARAMS(Group.DATA_NODE, "Empty params of a data node connection"), + + DATA_MAP_NO_NAME(Group.DATA_MAP, "Empty data map name"), + DATA_MAP_NAME_DUPLICATE(Group.DATA_MAP, "Duplicate of a data map name"), + DATA_MAP_NODE_LINKAGE(Group.DATA_MAP, "Data map is not linked to a data node"), + DATA_MAP_JAVA_PACKAGE(Group.DATA_MAP, "Invalid java package of a data map"), + + OBJ_ENTITY_NO_NAME(Group.OBJ_ENTITY, "Empty obj entity name"), + OBJ_ENTITY_NAME_DUPLICATE(Group.OBJ_ENTITY, "Duplicate of obj entity name"), + OBJ_ENTITY_NO_DB_ENTITY(Group.OBJ_ENTITY, "Obj entity has no db entity mapping"), + OBJ_ENTITY_INVALID_CLASS(Group.OBJ_ENTITY, "Obj entity has invalid Java class"), + OBJ_ENTITY_INVALID_SUPER_CLASS(Group.OBJ_ENTITY, "Obj entity has invalid Java super class"), + + OBJ_ATTRIBUTE_NO_NAME(Group.OBJ_ATTRIBUTE, "Empty obj attribute name"), + OBJ_ATTRIBUTE_INVALID_NAME(Group.OBJ_ATTRIBUTE, "Invalid obj entity name"), + OBJ_ATTRIBUTE_NO_TYPE(Group.OBJ_ATTRIBUTE, "Empty obj attribute type"), + OBJ_ATTRIBUTE_NO_EMBEDDABLE(Group.OBJ_ATTRIBUTE, "Embeddable obj attribute has no embeddable"), + OBJ_ATTRIBUTE_INVALID_MAPPING(Group.OBJ_ATTRIBUTE, "Obj attribute has invalid mapping to a db attribute"), + OBJ_ATTRIBUTE_PATH_DUPLICATE(Group.OBJ_ATTRIBUTE, "Duplicate of db path of obj attribute"), + OBJ_ATTRIBUTE_SUPER_NAME_DUPLICATE(Group.OBJ_ATTRIBUTE, "Duplicate of an obj attribute name in a super entity"), + + OBJ_RELATIONSHIP_NO_NAME(Group.OBJ_RELATIONSHIP, "Empty obj relationship name"), + OBJ_RELATIONSHIP_NAME_DUPLICATE(Group.OBJ_RELATIONSHIP, "Duplicate of an obj relationship name"), + OBJ_RELATIONSHIP_INVALID_NAME(Group.OBJ_RELATIONSHIP, "Invalid obj relationship name"), + OBJ_RELATIONSHIP_NO_TARGET(Group.OBJ_RELATIONSHIP, "No obj relationship target"), + OBJ_RELATIONSHIP_TARGET_NOT_PK(Group.OBJ_RELATIONSHIP, "Obj relationship target attribute is not a primary key"), + OBJ_RELATIONSHIP_INVALID_REVERSED(Group.OBJ_RELATIONSHIP, "Invalid reversed obj relationship"), + OBJ_RELATIONSHIP_SEMANTIC_DUPLICATE(Group.OBJ_RELATIONSHIP, "Obj relationships with same source and target entities"), + OBJ_RELATIONSHIP_INVALID_MAPPING(Group.OBJ_RELATIONSHIP, "Obj relationship has invalid mapping to a db relationship"), + OBJ_RELATIONSHIP_NULLIFY_NOT_NULL(Group.OBJ_RELATIONSHIP, "Nullify delete rule with a mandatory foreign key"), + OBJ_RELATIONSHIP_DUPLICATE_IN_ENTITY(Group.OBJ_RELATIONSHIP, "Duplicate of an obj relationship in the same obj entity"), + + DB_ENTITY_NO_NAME(Group.DB_ENTITY, "Empty db entity name"), + DB_ENTITY_NAME_DUPLICATE(Group.DB_ENTITY, "Duplicate of a db entity name"), + DB_ENTITY_NO_ATTRIBUTES(Group.DB_ENTITY, "Db entity has no attributes"), + DB_ENTITY_NO_PK(Group.DB_ENTITY, "Db entity has no primary key"), + + DB_ATTRIBUTE_NO_NAME(Group.DB_ATTRIBUTE, "Empty db attribute name"), + DB_ATTRIBUTE_INVALID_NAME(Group.DB_ATTRIBUTE, "Invalid db attribute name"), + DB_ATTRIBUTE_NO_TYPE(Group.DB_ATTRIBUTE, "Empty db attribute type"), + DB_ATTRIBUTE_NO_LENGTH(Group.DB_ATTRIBUTE, "String db attribute has no length"), + + DB_RELATIONSHIP_NO_NAME(Group.DB_RELATIONSHIP, "Empty db relationship name"), + DB_RELATIONSHIP_NAME_DUPLICATE(Group.DB_RELATIONSHIP, "Duplicate of a db relationship name"), + DB_RELATIONSHIP_INVALID_NAME(Group.DB_RELATIONSHIP, "Invalid db relationship name"), + DB_RELATIONSHIP_PATH_DUPLICATE(Group.DB_RELATIONSHIP, "Duplicate of a db relationship path"), + DB_RELATIONSHIP_NO_TARGET(Group.DB_RELATIONSHIP, "No db relationship target"), + DB_RELATIONSHIP_TARGET_NOT_PK(Group.DB_RELATIONSHIP, "Db relationship target attribute is not a primary key"), + DB_RELATIONSHIP_NO_JOINS(Group.DB_RELATIONSHIP, "No db relationship joins"), + DB_RELATIONSHIP_INVALID_JOIN(Group.DB_RELATIONSHIP, "Invalid db relationship join"), + DB_RELATIONSHIP_BOTH_TO_MANY(Group.DB_RELATIONSHIP, "Both db relationship and the reversed one are to-many"), + DB_RELATIONSHIP_DIFFERENT_TYPES(Group.DB_RELATIONSHIP, "Source and target db relationship attributes are of different types"), + DB_RELATIONSHIP_GENERATED_WITH_DEPENDENT_PK(Group.DB_RELATIONSHIP, "Db relationship target is a dependent generated primary key"), + + EMBEDDABLE_NO_NAME(Group.EMBEDDABLE, "Empty embeddable name"), + EMBEDDABLE_NAME_DUPLICATE(Group.EMBEDDABLE, "Duplicate of an embeddable name"), + + EMBEDDABLE_ATTRIBUTE_NO_NAME(Group.EMBEDDABLE_ATTRIBUTE, "Empty embeddable attribute name"), + EMBEDDABLE_ATTRIBUTE_NO_TYPE(Group.EMBEDDABLE_ATTRIBUTE, "Empty embeddable attribute type"), + + PROCEDURE_NO_NAME(Group.PROCEDURE, "Empty procedure name"), + PROCEDURE_NAME_DUPLICATE(Group.PROCEDURE, "Duplicate of procedure name"), + PROCEDURE_NO_PARAMS(Group.PROCEDURE, "Procedure returns a value but has no params"), + + PROCEDURE_PARAMETER_NO_NAME(Group.PROCEDURE_PARAMETER, "Empty procedure parameter name"), + PROCEDURE_PARAMETER_NO_TYPE(Group.PROCEDURE_PARAMETER, "Empty procedure parameter type"), + PROCEDURE_PARAMETER_NO_LENGTH(Group.PROCEDURE_PARAMETER, "String procedure parameter has no length"), + PROCEDURE_PARAMETER_NO_DIRECTION(Group.PROCEDURE_PARAMETER, "Procedure parameter has no direction"), + + QUERY_NO_NAME(Group.QUERY, "Empty query name"), + QUERY_NAME_DUPLICATE(Group.QUERY, "Duplicate of a query name"), + QUERY_MULTI_CACHE_GROUP(Group.QUERY, "Query has several cache groups"), + + SELECT_QUERY_NO_ROOT(Group.SELECT_QUERY, "Empty select query root"), + SELECT_QUERY_INVALID_QUALIFIER(Group.SELECT_QUERY, "Invalid select query qualifier", "Not implemented"), + SELECT_QUERY_INVALID_ORDERING_PATH(Group.SELECT_QUERY, "Invalid select query ordering path"), + SELECT_QUERY_INVALID_PREFETCH_PATH(Group.SELECT_QUERY, "Invalid select query prefetch path", "Not implemented"), + + PROCEDURE_QUERY_NO_ROOT(Group.PROCEDURE_QUERY, "Empty procedure query root"), + PROCEDURE_QUERY_INVALID_ROOT(Group.PROCEDURE_QUERY, "Invalid procedure query root"), + + EJBQL_QUERY_INVALID_SYNTAX(Group.EJBQL_QUERY, "Invalid syntax of an EJBQL query"), + + SQL_TEMPLATE_NO_ROOT(Group.SQL_TEMPLATE, "Empty SQL template query root"), + SQL_TEMPLATE_NO_DEFAULT_SQL(Group.SQL_TEMPLATE, "SQL template query has no default SQL template"); + + private final Group group; + private final String readableName; + private final String description; + + Inspection(Group group, String name) { + this(group, name, name); + } + + Inspection(Group group, String name, String description) { + this.group = group; + this.readableName = name; + this.description = description; + } + + public Group group() { + return group; + } + + public String readableName() { + return readableName; + } + + public String description() { + return description; + } + + @Override + public String toString() { + return readableName(); + } + + /** + * @since 5.0 + */ + public enum Group { + DATA_CHANNEL("Data domain"), + DATA_NODE("Data node"), + DATA_MAP("Data map"), + OBJ_ENTITY("Obj entity"), + OBJ_ATTRIBUTE("Obj attribute"), + OBJ_RELATIONSHIP("Obj relationship"), + DB_ENTITY("Db entity"), + DB_ATTRIBUTE("Db attribute"), + DB_RELATIONSHIP("Db relationship"), + EMBEDDABLE("Embeddable"), + EMBEDDABLE_ATTRIBUTE("Embeddable attribute"), + PROCEDURE("Procedure"), + PROCEDURE_PARAMETER("Procedure parameter"), + QUERY("Query"), + SELECT_QUERY("Select query"), + PROCEDURE_QUERY("Procedure query"), + EJBQL_QUERY("EJBQL query"), + SQL_TEMPLATE("SQL template"); + + private final String readableName; + + Group(String readableName) { + this.readableName = readableName; + } + + public String readableName() { + return readableName; + } + + @Override + public String toString() { + return readableName(); + } + } +} diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ObjAttributeValidator.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ObjAttributeValidator.java index 3c89340d5b..9db14bd420 100644 --- a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ObjAttributeValidator.java +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ObjAttributeValidator.java @@ -25,37 +25,40 @@ import org.apache.cayenne.validation.ValidationResult; import java.util.Map; +import java.util.function.Supplier; -class ObjAttributeValidator extends ConfigurationNodeValidator { +class ObjAttributeValidator extends ConfigurationNodeValidator { - void validate(ObjAttribute attribute, ValidationResult validationResult) { - - validateName(attribute, validationResult); - - // all attributes must have type - if (Util.isEmptyString(attribute.getType())) { - addFailure(validationResult, attribute, - "ObjAttribute '%s' has no Java type", - attribute.getName()); - } - - if (attribute instanceof EmbeddedAttribute) { - validateEmbeddable((EmbeddedAttribute)attribute, validationResult); - } else { - validateDbAttribute(attribute, validationResult); - } + /** + * @param configSupplier the config defining the behavior of this validator. + * @since 5.0 + */ + public ObjAttributeValidator(Supplier configSupplier) { + super(configSupplier); + } - checkForDuplicates(attribute, validationResult); - checkSuperEntityAttributes(attribute, validationResult); + @Override + public void validate(ObjAttribute node, ValidationResult validationResult) { + on(node, validationResult) + .performIfEnabled(Inspection.OBJ_ATTRIBUTE_NO_NAME, this::checkForName) + .performIfEnabled(Inspection.OBJ_ATTRIBUTE_INVALID_NAME, this::validateName) + .performIfEnabled(Inspection.OBJ_ATTRIBUTE_NO_TYPE, this::checkForType) + .performIfEnabled(Inspection.OBJ_ATTRIBUTE_NO_EMBEDDABLE, this::checkForEmbeddable) + .performIfEnabled(Inspection.OBJ_ATTRIBUTE_INVALID_MAPPING, this::validateDbAttributeMapping) + .performIfEnabled(Inspection.OBJ_ATTRIBUTE_PATH_DUPLICATE, this::checkForPathDuplicates) + .performIfEnabled(Inspection.OBJ_ATTRIBUTE_SUPER_NAME_DUPLICATE, + this::checkOnNameDuplicatesInSuperEntity); } - private void validateName(ObjAttribute attribute, ValidationResult validationResult) { + private void checkForName(ObjAttribute attribute, ValidationResult validationResult) { + // Must have name if (Util.isEmptyString(attribute.getName())) { addFailure(validationResult, attribute, "Unnamed ObjAttribute"); - return; } + } + private void validateName(ObjAttribute attribute, ValidationResult validationResult) { NameValidationHelper helper = NameValidationHelper.getInstance(); String invalidChars = helper.invalidCharsInObjPathComponent(attribute.getName()); @@ -71,19 +74,56 @@ private void validateName(ObjAttribute attribute, ValidationResult validationRes } } - private void checkSuperEntityAttributes(ObjAttribute attribute, ValidationResult validationResult) { - // Check there is an attribute in entity and super entity at the same time + private void checkForType(ObjAttribute attribute, ValidationResult validationResult) { - boolean selfAttribute = attribute.getEntity().getDeclaredAttribute(attribute.getName()) != null; + // all attributes must have type + if (Util.isEmptyString(attribute.getType())) { + addFailure(validationResult, attribute, "ObjAttribute '%s' has no Java type", attribute.getName()); + } + } - ObjEntity superEntity = attribute.getEntity().getSuperEntity(); - if (selfAttribute && superEntity != null && superEntity.getAttribute(attribute.getName()) != null) { - addFailure(validationResult, attribute, "'%s' and super '%s' can't both have attribute '%s'", - attribute.getEntity().getName(), superEntity.getName(), attribute.getName()); + private void checkForEmbeddable(ObjAttribute attribute, ValidationResult validationResult) { + if (!(attribute instanceof EmbeddedAttribute)) { + return; + } + Embeddable embeddable = ((EmbeddedAttribute) attribute).getEmbeddable(); + if (embeddable == null) { + String msg = attribute.getType() == null + ? "EmbeddedAttribute '%s' has no Embeddable" + : "EmbeddedAttribute '%s' has incorrect Embeddable"; + addFailure(validationResult, attribute, msg, attribute.getName()); } } - private void validateDbAttribute(ObjAttribute attribute, ValidationResult validationResult) { + private void validateDbAttributeMapping(ObjAttribute attribute, ValidationResult validationResult) { + if (attribute instanceof EmbeddedAttribute) { + validateDbAttributeMappingEmbedded(((EmbeddedAttribute) attribute), validationResult); + } else { + validateDbAttributeMappingGeneral(attribute, validationResult); + } + } + + private void validateDbAttributeMappingEmbedded(EmbeddedAttribute attribute, ValidationResult validationResult) { + Embeddable embeddable = attribute.getEmbeddable(); + if (embeddable == null) { + return; + } + Map attrOverrides = attribute.getAttributeOverrides(); + for (EmbeddableAttribute embeddableAttribute : embeddable.getAttributes()) { + String dbAttributeName = attrOverrides.containsKey(embeddableAttribute.getName()) + ? attrOverrides.get(embeddableAttribute.getName()) + : embeddableAttribute.getDbAttributeName(); + if (Util.isEmptyString(dbAttributeName)) { + addFailure(validationResult, attribute, "EmbeddedAttribute '%s' has no DbAttribute mapping", + attribute.getName()); + } else if (attribute.getEntity().getDbEntity().getAttribute(dbAttributeName) == null) { + addFailure(validationResult, attribute, "EmbeddedAttribute '%s' has invalid DbAttribute mapping", + attribute.getName()); + } + } + } + + private void validateDbAttributeMappingGeneral(ObjAttribute attribute, ValidationResult validationResult) { if (attribute.getEntity().isAbstract()) { // nothing to validate // abstract entity does not have to define a dbAttribute @@ -97,16 +137,13 @@ private void validateDbAttribute(ObjAttribute attribute, ValidationResult valida // see CAY-2153 // getDbAttribute() can fail if db path for this attribute is invalid // so we catch it here and show nice validation failure instead of crash - addFailure(validationResult, attribute, - "ObjAttribute '%s' has invalid DB path: %s", - attribute.getName(), - e.getExpressionString()); + addFailure(validationResult, attribute, "ObjAttribute '%s' has invalid DB path: %s", + attribute.getName(), e.getExpressionString()); return; } if (dbAttribute == null) { - addFailure(validationResult, attribute, - "ObjAttribute '%s' has no DbAttribute mapping", + addFailure(validationResult, attribute, "ObjAttribute '%s' has no DbAttribute mapping", attribute.getName()); return; } @@ -115,75 +152,43 @@ private void validateDbAttribute(ObjAttribute attribute, ValidationResult valida // can't support generated meaningful attributes for now; // besides they don't make sense. // TODO: andrus 03/10/2010 - is that really so? I think those are supported... - addFailure(validationResult, attribute, - "ObjAttribute '%s' is mapped to a generated PK: %s", - attribute.getName(), - attribute.getDbAttributeName()); + addFailure(validationResult, attribute, "ObjAttribute '%s' is mapped to a generated PK: %s", + attribute.getName(), attribute.getDbAttributeName()); } } - private void validateEmbeddable(EmbeddedAttribute attribute, ValidationResult validationResult) { - Embeddable embeddable = attribute.getEmbeddable(); - - if (embeddable == null) { - String msg = attribute.getType() == null ? - "EmbeddedAttribute '%s' has no Embeddable" : - "EmbeddedAttribute '%s' has incorrect Embeddable"; - - addFailure(validationResult, attribute, msg, attribute.getName()); + /** + * Per CAY-1813, make sure two (or more) ObjAttributes do not map to the + * same database path. + */ + private void checkForPathDuplicates(ObjAttribute attribute, ValidationResult validationResult) { + if (attribute == null || attribute.getName() == null || attribute.isInherited()) { return; } - Map attrOverrides = attribute.getAttributeOverrides(); - - for (EmbeddableAttribute embeddableAttribute : embeddable.getAttributes()) { - String dbAttributeName; - if (!attrOverrides.isEmpty() - && attrOverrides.containsKey(embeddableAttribute.getName())) { - dbAttributeName = attrOverrides.get(embeddableAttribute.getName()); - } else { - dbAttributeName = embeddableAttribute.getDbAttributeName(); - } + ObjEntity entity = attribute.getEntity(); + CayennePath dbAttributePath = attribute.getDbAttributePath(); - if (Util.isEmptyString(dbAttributeName)) { + for (ObjAttribute comparisonAttribute : entity.getAttributes()) { + if (attribute != comparisonAttribute + && dbAttributePath != null + && dbAttributePath.equals(comparisonAttribute.getDbAttributePath())) { addFailure(validationResult, attribute, - "EmbeddedAttribute '%s' has no DbAttribute mapping", - attribute.getName()); - } else if (attribute.getEntity() - .getDbEntity() - .getAttribute(dbAttributeName) == null) { - addFailure(validationResult, attribute, - "EmbeddedAttribute '%s' has incorrect DbAttribute mapping", - attribute.getName()); + "ObjEntity '%s' contains a duplicate DbAttribute mapping ('%s' -> '%s')", + entity.getName(), attribute.getName(), dbAttributePath); + return; // Duplicate found, stop. } } } - /** - * Per CAY-1813, make sure two (or more) ObjAttributes do not map to the - * same database path. - */ - private void checkForDuplicates(ObjAttribute attribute, - ValidationResult validationResult) { - if (attribute != null - && attribute.getName() != null - && !attribute.isInherited()) { - - ObjEntity entity = attribute.getEntity(); - CayennePath dbAttributePath = attribute.getDbAttributePath(); - - for (ObjAttribute comparisonAttribute : entity.getAttributes()) { - if (attribute != comparisonAttribute - && dbAttributePath != null - && dbAttributePath.equals(comparisonAttribute.getDbAttributePath())) { - addFailure(validationResult, attribute, - "ObjEntity '%s' contains a duplicate DbAttribute mapping ('%s' -> '%s')", - entity.getName(), - attribute.getName(), - dbAttributePath); - return; // Duplicate found, stop. - } - } + private void checkOnNameDuplicatesInSuperEntity(ObjAttribute attribute, ValidationResult validationResult) { + + // Check there is an attribute in entity and super entity at the same time + boolean selfAttribute = attribute.getEntity().getDeclaredAttribute(attribute.getName()) != null; + ObjEntity superEntity = attribute.getEntity().getSuperEntity(); + if (selfAttribute && superEntity != null && superEntity.getAttribute(attribute.getName()) != null) { + addFailure(validationResult, attribute, "'%s' and super '%s' can't both have attribute '%s'", + attribute.getEntity().getName(), superEntity.getName(), attribute.getName()); } } } diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ObjEntityValidator.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ObjEntityValidator.java index 64333e6300..fca41fcc70 100644 --- a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ObjEntityValidator.java +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ObjEntityValidator.java @@ -24,25 +24,83 @@ import org.apache.cayenne.util.Util; import org.apache.cayenne.validation.ValidationResult; -class ObjEntityValidator extends ConfigurationNodeValidator { +import java.util.function.Supplier; - void validate(ObjEntity entity, ValidationResult validationResult) { +class ObjEntityValidator extends ConfigurationNodeValidator { - validateName(entity, validationResult); - validateClassName(entity, validationResult); - validateSuperClassName(entity, validationResult); + /** + * @param configSupplier the config defining the behavior of this validator. + * @since 5.0 + */ + public ObjEntityValidator(Supplier configSupplier) { + super(configSupplier); + } + + @Override + public void validate(ObjEntity node, ValidationResult validationResult) { + on(node, validationResult) + .performIfEnabled(Inspection.OBJ_ENTITY_NO_NAME, this::checkForName) + .performIfEnabled(Inspection.OBJ_ENTITY_NAME_DUPLICATE, this::checkForNameDuplicates) + .performIfEnabled(Inspection.OBJ_ENTITY_NO_DB_ENTITY, this::checkForDbEntity) + .performIfEnabled(Inspection.OBJ_ENTITY_INVALID_CLASS, this::validateClassName) + .performIfEnabled(Inspection.OBJ_ENTITY_INVALID_SUPER_CLASS, this::validateSuperClassName); + } + + private void checkForName(ObjEntity entity, ValidationResult validationResult) { + + // Must have name + String name = entity.getName(); + if (Util.isEmptyString(name)) { + addFailure(validationResult, entity, "Unnamed ObjEntity"); + } + } + + private void checkForNameDuplicates(ObjEntity entity, ValidationResult validationResult) { + String name = entity.getName(); + DataMap map = entity.getDataMap(); + if (map == null || Util.isEmptyString(name)) { + return; + } + + // check for duplicate names in the parent context + for (ObjEntity otherEnt : map.getObjEntities()) { + if (otherEnt == entity) { + continue; + } + if (name.equals(otherEnt.getName())) { + addFailure(validationResult, entity, "Duplicate ObjEntity name: '%s'", name); + break; + } + } + + // check for duplicates in other DataMaps + DataChannelDescriptor domain = entity.getDataMap().getDataChannelDescriptor(); + if (domain == null) { + return; + } + for (DataMap nextMap : domain.getDataMaps()) { + if (nextMap == map) { + continue; + } + + ObjEntity conflictingEntity = nextMap.getObjEntity(name); + if (conflictingEntity != null + && !Util.nullSafeEquals(conflictingEntity.getClassName(), entity.getClassName())) { + addFailure(validationResult, entity, "Duplicate ObjEntity name in another DataMap: '%s'", name); + break; + } + } + } + + private void checkForDbEntity(ObjEntity entity, ValidationResult validationResult) { // validate DbEntity presence if (entity.getDbEntity() == null && !entity.isAbstract()) { - addFailure( - validationResult, - entity, - "ObjEntity '%s' has no DbEntity mapping", - entity.getName()); + addFailure(validationResult, entity, "ObjEntity '%s' has no DbEntity mapping", entity.getName()); } } - void validateClassName(ObjEntity entity, ValidationResult validationResult) { + private void validateClassName(ObjEntity entity, ValidationResult validationResult) { String className = entity.getClassName(); // if mapped to default class, ignore... @@ -54,31 +112,18 @@ void validateClassName(ObjEntity entity, ValidationResult validationResult) { String invalidChars = helper.invalidCharsInJavaClassName(className); if (invalidChars != null) { - addFailure( - validationResult, - entity, - "ObjEntity '%s' Java class '%s' contains invalid characters: %s", - entity.getName(), - className, - invalidChars); + addFailure(validationResult, entity, "ObjEntity '%s' Java class '%s' contains invalid characters: %s", + entity.getName(), className, invalidChars); } else if (helper.invalidPersistentObjectClass(className)) { - addFailure( - validationResult, - entity, - "Java class '%s' of ObjEntity '%s' is a reserved word", - className, - entity.getName()); + addFailure(validationResult, entity, "Java class '%s' of ObjEntity '%s' is a reserved word", + className, entity.getName()); } else if (className.indexOf('.') < 0) { - addFailure( - validationResult, - entity, - "Java class '%s' of ObjEntity '%s' is in a default package", - className, - entity.getName()); + addFailure(validationResult, entity, "Java class '%s' of ObjEntity '%s' is in a default package", + className, entity.getName()); } } - void validateSuperClassName(ObjEntity entity, ValidationResult validationResult) { + private void validateSuperClassName(ObjEntity entity, ValidationResult validationResult) { String superClassName = entity.getSuperClassName(); if (Util.isEmptyString(superClassName)) { @@ -89,84 +134,17 @@ void validateSuperClassName(ObjEntity entity, ValidationResult validationResult) String invalidChars = helper.invalidCharsInJavaClassName(superClassName); if (invalidChars != null) { - addFailure( - validationResult, - entity, - "ObjEntity '%s' Java superclass '%s' contains invalid characters: %s", - entity.getName(), - superClassName, - invalidChars); + addFailure(validationResult, entity, "ObjEntity '%s' Java superclass '%s' contains invalid characters: %s", + entity.getName(), superClassName, invalidChars); } else if (helper.invalidPersistentObjectClass(superClassName)) { - addFailure( - validationResult, - entity, - "ObjEntity '%s' Java superclass '%s' is a reserved word", - entity.getName(), - superClassName); + addFailure(validationResult, entity, "ObjEntity '%s' Java superclass '%s' is a reserved word", + entity.getName(), superClassName); } if (entity.getDbEntityName() != null && entity.getSuperEntityName() != null) { - addFailure( - validationResult, - entity, + addFailure(validationResult, entity, "Sub ObjEntity '%s' has database table declaration different from super ObjEntity '%s'", - entity.getName(), - entity.getSuperEntityName()); - } - } - - void validateName(ObjEntity entity, ValidationResult validationResult) { - String name = entity.getName(); - - // Must have name - if (Util.isEmptyString(name)) { - addFailure(validationResult, entity, "Unnamed ObjEntity"); - return; - } - - DataMap map = entity.getDataMap(); - if (map == null) { - return; - } - - // check for duplicate names in the parent context - for (ObjEntity otherEnt : map.getObjEntities()) { - if (otherEnt == entity) { - continue; - } - - if (name.equals(otherEnt.getName())) { - addFailure( - validationResult, - entity, - "Duplicate ObjEntity name: '%s'", - name); - break; - } - } - - // check for duplicates in other DataMaps - DataChannelDescriptor domain = entity.getDataMap().getDataChannelDescriptor(); - if (domain != null) { - for (DataMap nextMap : domain.getDataMaps()) { - if (nextMap == map) { - continue; - } - - ObjEntity conflictingEntity = nextMap.getObjEntity(name); - if (conflictingEntity != null) { - - if (!Util.nullSafeEquals(conflictingEntity.getClassName(), entity - .getClassName())) { - addFailure( - validationResult, - entity, - "Duplicate ObjEntity name in another DataMap: '%s'", - name); - break; - } - } - } + entity.getName(), entity.getSuperEntityName()); } } } diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ObjRelationshipValidator.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ObjRelationshipValidator.java index 5c09570d47..25cebcb5a4 100644 --- a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ObjRelationshipValidator.java +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ObjRelationshipValidator.java @@ -29,180 +29,186 @@ import java.util.Iterator; import java.util.List; +import java.util.function.Supplier; -class ObjRelationshipValidator extends ConfigurationNodeValidator { +class ObjRelationshipValidator extends ConfigurationNodeValidator { - void validate(ObjRelationship relationship, ValidationResult validationResult) { + /** + * @param configSupplier the config defining the behavior of this validator. + * @since 5.0 + */ + public ObjRelationshipValidator(Supplier configSupplier) { + super(configSupplier); + } + @Override + public void validate(ObjRelationship node, ValidationResult validationResult) { + on(node, validationResult) + .performIfEnabled(Inspection.OBJ_RELATIONSHIP_NO_NAME, this::checkForName) + .performIfEnabled(Inspection.OBJ_RELATIONSHIP_NAME_DUPLICATE, this::checkForNameDuplicates) + .performIfEnabled(Inspection.OBJ_RELATIONSHIP_INVALID_NAME, this::validateName) + .performIfEnabled(Inspection.OBJ_RELATIONSHIP_NO_TARGET, this::checkForTarget) + .performIfEnabled(Inspection.OBJ_RELATIONSHIP_TARGET_NOT_PK, this::checkForPK) + .performIfEnabled(Inspection.OBJ_RELATIONSHIP_INVALID_REVERSED, this::validateReverse) + .performIfEnabled(Inspection.OBJ_RELATIONSHIP_SEMANTIC_DUPLICATE, this::checkForSemanticDuplicates) + .performIfEnabled(Inspection.OBJ_RELATIONSHIP_INVALID_MAPPING, this::validateMapping) + .performIfEnabled(Inspection.OBJ_RELATIONSHIP_NULLIFY_NOT_NULL, this::validateDeleteRule) + .performIfEnabled(Inspection.OBJ_RELATIONSHIP_DUPLICATE_IN_ENTITY, this::checkForDuplicatesInEntity); + } + + private void checkForName(ObjRelationship relationship, ValidationResult validationResult) { if (Util.isEmptyString(relationship.getName())) { addFailure(validationResult, relationship, "Unnamed ObjRelationship"); - } else if (relationship.getSourceEntity().getAttribute(relationship.getName()) != null) { + } + } + + private void checkForNameDuplicates(ObjRelationship relationship, ValidationResult validationResult) { + if (relationship.getSourceEntity().getAttribute(relationship.getName()) != null) { // check if there are attributes having the same name - addFailure( - validationResult, - relationship, - "ObjRelationship '%s' has the same name as one of ObjAttributes", + addFailure(validationResult, relationship, + "ObjRelationship '%s' has the same name as one of ObjAttributes", toString(relationship)); + } + } + + private void validateName(ObjRelationship relationship, ValidationResult validationResult) { + NameValidationHelper helper = NameValidationHelper.getInstance(); + String invalidChars = helper.invalidCharsInObjPathComponent(relationship.getName()); + + if (invalidChars != null) { + addFailure(validationResult, relationship, "ObjRelationship name '%s' contains invalid characters: %s", + toString(relationship), invalidChars); + } else if (helper.invalidPersistentObjectProperty(relationship.getName())) { + addFailure(validationResult, relationship, "ObjRelationship name '%s' is a reserved word", toString(relationship)); - } else { - NameValidationHelper helper = NameValidationHelper.getInstance(); - String invalidChars = helper.invalidCharsInObjPathComponent(relationship - .getName()); - - if (invalidChars != null) { - addFailure( - validationResult, - relationship, - "ObjRelationship name '%s' contains invalid characters: %s", - toString(relationship), - invalidChars); - } else if (helper.invalidPersistentObjectProperty(relationship.getName())) { - addFailure( - validationResult, - relationship, - "ObjRelationship name '%s' is a reserved word", - toString(relationship)); - } } + } + private void checkForTarget(ObjRelationship relationship, ValidationResult validationResult) { if (relationship.getTargetEntity() == null) { - addFailure( - validationResult, - relationship, - "ObjRelationship '%s' has no target entity", + addFailure(validationResult, relationship, "ObjRelationship '%s' has no target entity", toString(relationship)); - } else { - - // check for missing DbRelationship mappings - List dbRels = relationship.getDbRelationships(); - if (dbRels.isEmpty()) { - addFailure( - validationResult, - relationship, - "ObjRelationship '%s' has no DbRelationship mapping", - toString(relationship)); - } else { - DbEntity expectedSrc = relationship.getSourceEntity().getDbEntity(); - DbEntity expectedTarget = relationship.getTargetEntity().getDbEntity(); - - if ((dbRels.get(0)).getSourceEntity() != expectedSrc - || (dbRels.get(dbRels.size() - 1)).getTargetEntity() != expectedTarget) { - - addFailure( - validationResult, - relationship, - "ObjRelationship '%s' has incomplete DbRelationship mapping", - toString(relationship)); - } - } } + } - // Disallow a Nullify delete rule where the relationship is toMany and the - // foreign key attributes are mandatory. - if (relationship.isToMany() - && !relationship.isFlattened() - && (relationship.getDeleteRule() == DeleteRule.NULLIFY)) { - ObjRelationship inverse = relationship.getReverseRelationship(); - if (inverse != null) { - DbRelationship firstRel = inverse.getDbRelationships().get(0); - Iterator attributePairIterator = firstRel.getJoins().iterator(); - // by default, the relation will be check for mandatory. - boolean check = true; - while (attributePairIterator.hasNext()) { - DbJoin pair = attributePairIterator.next(); - if (!pair.getSource().isMandatory()) { - // a field of the fk can be nullable, cancel the check. - check = false; - break; - } - } - - if (check) { - addFailure( - validationResult, - relationship, - "ObjRelationship '%s' has a Nullify delete rule and a mandatory reverse relationship", - toString(relationship)); - } - } + private void checkForPK(ObjRelationship relationship, ValidationResult validationResult) { + if (relationship.getDbRelationships().isEmpty() || relationship.isToPK()) { + return; } + ObjRelationship reverseRelationship = relationship.getReverseRelationship(); + if (reverseRelationship != null + && !relationship.getDbRelationships().isEmpty() + && !reverseRelationship.isToPK()) { + addFailure(validationResult, relationship, + "ObjRelationship '%s' has join not to PK. This is not fully supported by Cayenne.", + toString(relationship)); + } + } - if(!relationship.getDbRelationships().isEmpty() && !relationship.isToPK()) { - ObjRelationship reverseRelationship = relationship.getReverseRelationship(); - if(reverseRelationship != null && !relationship.getDbRelationships().isEmpty() && !reverseRelationship.isToPK()) { - addFailure( - validationResult, - relationship, - "ObjRelationship '%s' has join not to PK. This is not fully supported by Cayenne.", - toString(relationship)); - } + private void validateReverse(ObjRelationship relationship, ValidationResult validationResult) { + + // check for invalid relationships in inherited entities + if (relationship.getReverseRelationship() == null) { + return; + } + ObjRelationship revRel = relationship.getReverseRelationship(); + if (relationship.getSourceEntity() != revRel.getTargetEntity() + || relationship.getTargetEntity() != revRel.getSourceEntity()) { + addFailure(validationResult, revRel, + "Usage of super entity's relationships '%s' as reversed relationships for sub entity is discouraged", + toString(revRel)); } + } + + private void checkForSemanticDuplicates(ObjRelationship relationship, ValidationResult validationResult) { // check for relationships with same source and target entities ObjEntity entity = relationship.getSourceEntity(); - for (ObjRelationship rel : entity.getRelationships()) { - if (relationship.getDbRelationshipPath() != null && relationship.getDbRelationshipPath().equals(rel.getDbRelationshipPath())) { - if (relationship != rel && - relationship.getTargetEntity() == rel.getTargetEntity() && - relationship.getSourceEntity() == rel.getSourceEntity()) { - addFailure( - validationResult, - relationship, - "ObjectRelationship '%s' duplicates relationship '%s'", - toString(relationship), - toString(rel)); - } + for (ObjRelationship otherRelationship : entity.getRelationships()) { + if (relationship.getDbRelationshipPath() != null + && relationship != otherRelationship + && relationship.getTargetEntity() == otherRelationship.getTargetEntity() + && relationship.getSourceEntity() == otherRelationship.getSourceEntity() + && relationship.getDbRelationshipPath().equals(otherRelationship.getDbRelationshipPath())) { + addFailure(validationResult, relationship, "ObjectRelationship '%s' duplicates relationship '%s'", + toString(relationship), toString(otherRelationship)); } } + } - // check for invalid relationships in inherited entities - if (relationship.getReverseRelationship() != null) { - ObjRelationship revRel = relationship.getReverseRelationship(); - if (relationship.getSourceEntity() != revRel.getTargetEntity() - || relationship.getTargetEntity() != revRel.getSourceEntity()) { - addFailure( - validationResult, - revRel, - "Usage of super entity's relationships '%s' as reversed relationships for sub entity is discouraged", - toString(revRel)); + private void validateMapping(ObjRelationship relationship, ValidationResult validationResult) { + + // check for missing DbRelationship mappings + List dbRels = relationship.getDbRelationships(); + if (dbRels.isEmpty()) { + addFailure(validationResult, relationship, "ObjRelationship '%s' has no DbRelationship mapping", + toString(relationship)); + } else { + DbEntity expectedSrc = relationship.getSourceEntity().getDbEntity(); + DbEntity expectedTarget = relationship.getTargetEntity().getDbEntity(); + if ((dbRels.get(0)).getSourceEntity() != expectedSrc + || (dbRels.get(dbRels.size() - 1)).getTargetEntity() != expectedTarget) { + addFailure(validationResult, relationship, + "ObjRelationship '%s' has incomplete DbRelationship mapping", + toString(relationship)); } } + } - checkForDuplicates(relationship, validationResult); + private void validateDeleteRule(ObjRelationship relationship, ValidationResult validationResult) { + // Disallow a Nullify delete rule where the relationship is toMany and the + // foreign key attributes are mandatory. + if (!relationship.isToMany() + || relationship.isFlattened() + || relationship.getDeleteRule() != DeleteRule.NULLIFY) { + return; + } + ObjRelationship inverse = relationship.getReverseRelationship(); + if (inverse == null) { + return; + } + DbRelationship firstRel = inverse.getDbRelationships().get(0); + Iterator attributePairIterator = firstRel.getJoins().iterator(); + // by default, the relation will be check for mandatory. + boolean check = true; + while (attributePairIterator.hasNext()) { + DbJoin pair = attributePairIterator.next(); + if (!pair.getSource().isMandatory()) { + // a field of the fk can be nullable, cancel the check. + check = false; + break; + } + } + + if (check) { + addFailure(validationResult, relationship, + "ObjRelationship '%s' has a Nullify delete rule and a mandatory reverse relationship", + toString(relationship)); + } } /** * Per CAY-1813, make sure two (or more) ObjRelationships do not map to the * same database path. */ - private void checkForDuplicates(ObjRelationship relationship, ValidationResult validationResult) { - if (relationship != null && - relationship.getName() != null && - relationship.getTargetEntityName() != null) { - - String dbRelationshipPath = - relationship.getTargetEntityName() + - "." + - relationship.getDbRelationshipPath(); - - ObjEntity entity = relationship.getSourceEntity(); - - for (ObjRelationship comparisonRelationship : entity.getRelationships()) { - if (relationship != comparisonRelationship) { - String comparisonDbRelationshipPath = - comparisonRelationship.getTargetEntityName() + - "." + - comparisonRelationship.getDbRelationshipPath(); - - if (dbRelationshipPath.equals(comparisonDbRelationshipPath)) { - addFailure(validationResult, - relationship, - "ObjEntity '%s' contains a duplicate ObjRelationship mapping ('%s' -> '%s')", - entity.getName(), - relationship.getName(), - dbRelationshipPath); - return; // Duplicate found, stop. - } - } + private void checkForDuplicatesInEntity(ObjRelationship relationship, ValidationResult validationResult) { + if (relationship == null || relationship.getName() == null || relationship.getTargetEntityName() == null) { + return; + } + + String dbRelationshipPath = relationship.getTargetEntityName() + "." + relationship.getDbRelationshipPath(); + ObjEntity entity = relationship.getSourceEntity(); + for (ObjRelationship otherRelationship : entity.getRelationships()) { + if (relationship == otherRelationship) { + continue; + } + String otherDbRelationshipPath = otherRelationship.getTargetEntityName() + + "." + otherRelationship.getDbRelationshipPath(); + + if (dbRelationshipPath.equals(otherDbRelationshipPath)) { + addFailure(validationResult, relationship, + "ObjEntity '%s' contains a duplicate ObjRelationship mapping ('%s' -> '%s')", + entity.getName(), relationship.getName(), dbRelationshipPath); + return; // Duplicate found, stop. } } } @@ -214,5 +220,4 @@ private String toString(ObjRelationship relationship) { return relationship.getSourceEntity().getName() + "." + relationship.getName(); } - } diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ProcedureParameterValidator.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ProcedureParameterValidator.java index b354d903e6..dcf754f5df 100644 --- a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ProcedureParameterValidator.java +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ProcedureParameterValidator.java @@ -19,30 +19,47 @@ package org.apache.cayenne.project.validation; import java.sql.Types; +import java.util.function.Supplier; import org.apache.cayenne.dba.TypesMapping; import org.apache.cayenne.map.ProcedureParameter; import org.apache.cayenne.util.Util; import org.apache.cayenne.validation.ValidationResult; -class ProcedureParameterValidator extends ConfigurationNodeValidator { +class ProcedureParameterValidator extends ConfigurationNodeValidator { - void validate(ProcedureParameter parameter, ValidationResult validationResult) { + /** + * @param configSupplier the config defining the behavior of this validator. + * @since 5.0 + */ + public ProcedureParameterValidator(Supplier configSupplier) { + super(configSupplier); + } + + @Override + public void validate(ProcedureParameter node, ValidationResult validationResult) { + on(node, validationResult) + .performIfEnabled(Inspection.PROCEDURE_PARAMETER_NO_NAME, this::checkForName) + .performIfEnabled(Inspection.PROCEDURE_PARAMETER_NO_TYPE, this::checkForType) + .performIfEnabled(Inspection.PROCEDURE_PARAMETER_NO_LENGTH, this::checkForLength) + .performIfEnabled(Inspection.PROCEDURE_PARAMETER_NO_DIRECTION, this::checkForDirection); + } + private void checkForName(ProcedureParameter parameter, ValidationResult validationResult) { // Must have name if (Util.isEmptyString(parameter.getName())) { addFailure(validationResult, parameter, "Unnamed ProcedureParameter"); } + } + private void checkForType(ProcedureParameter parameter, ValidationResult validationResult) { // all attributes must have type if (parameter.getType() == TypesMapping.NOT_DEFINED) { - addFailure( - validationResult, - parameter, - "ProcedureParameter '%s' has no type", - parameter.getName()); + addFailure(validationResult, parameter, "ProcedureParameter '%s' has no type", parameter.getName()); } + } + private void checkForLength(ProcedureParameter parameter, ValidationResult validationResult) { // VARCHAR and CHAR attributes must have max length if (parameter.getMaxLength() < 0 && (parameter.getType() == Types.VARCHAR @@ -50,20 +67,15 @@ void validate(ProcedureParameter parameter, ValidationResult validationResult) { || parameter.getType() == Types.CHAR || parameter.getType() == Types.NCHAR)) { - addFailure( - validationResult, - parameter, - "Character ProcedureParameter '%s' doesn't have max length", + addFailure(validationResult, parameter, "Character ProcedureParameter '%s' doesn't have max length", parameter.getName()); } + } + private void checkForDirection(ProcedureParameter parameter, ValidationResult validationResult) { // all attributes must have type if (parameter.getDirection() <= 0) { - addFailure( - validationResult, - parameter, - "ProcedureParameter '%s' has no direction", - parameter.getName()); + addFailure(validationResult, parameter, "ProcedureParameter '%s' has no direction", parameter.getName()); } } } diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ProcedureQueryValidator.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ProcedureQueryValidator.java index 64c89990d1..4f516ecb28 100644 --- a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ProcedureQueryValidator.java +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ProcedureQueryValidator.java @@ -23,22 +23,40 @@ import org.apache.cayenne.map.ProcedureQueryDescriptor; import org.apache.cayenne.validation.ValidationResult; -class ProcedureQueryValidator extends BaseQueryValidator { +import java.util.function.Supplier; - void validate(ProcedureQueryDescriptor query, ValidationResult validationResult) { - validateName(query, validationResult); - validateRoot(query, validationResult); - validateCacheGroup(query, validationResult); +class ProcedureQueryValidator extends BaseQueryValidator { + + /** + * @param configSupplier the config defining the behavior of this validator. + * @since 5.0 + */ + public ProcedureQueryValidator(Supplier configSupplier) { + super(configSupplier); } - void validateRoot(ProcedureQueryDescriptor query, ValidationResult validationResult) { + @Override + protected ConfigurationNodeValidator.Performer validateQuery( + ProcedureQueryDescriptor query, ValidationResult validationResult) { + return super.validateQuery(query, validationResult) + .performIfEnabled(Inspection.PROCEDURE_QUERY_NO_ROOT, this::checkForRoot) + .performIfEnabled(Inspection.PROCEDURE_QUERY_INVALID_ROOT, this::validateRoot); + } + private void checkForRoot(ProcedureQueryDescriptor query, ValidationResult validationResult) { DataMap map = query.getDataMap(); Object root = query.getRoot(); if (root == null && map != null) { - addFailure(validationResult, query, "ProcedureQuery '%s' has no root", query - .getName()); + addFailure(validationResult, query, "ProcedureQuery '%s' has no root", query.getName()); + } + } + + private void validateRoot(ProcedureQueryDescriptor query, ValidationResult validationResult) { + DataMap map = query.getDataMap(); + Object root = query.getRoot(); + if (root == null) { + return; } // procedure query only supports procedure root @@ -47,25 +65,13 @@ void validateRoot(ProcedureQueryDescriptor query, ValidationResult validationRes // procedure may have been deleted... if (map != null && map.getProcedure(procedure.getName()) != procedure) { - addFailure( - validationResult, - query, - "ProcedureQuery '%s' has invalid Procedure root: %s", - query.getName(), - procedure.getName()); + addFailure(validationResult, query, "ProcedureQuery '%s' has invalid Procedure root: %s", + query.getName(), procedure.getName()); } - - return; - } - - if (root instanceof String) { + } else if (root instanceof String) { if (map != null && map.getProcedure(root.toString()) == null) { - addFailure( - validationResult, - query, - "ProcedureQuery '%s' has invalid Procedure root: %s", - query.getName(), - root); + addFailure(validationResult, query, "ProcedureQuery '%s' has invalid Procedure root: %s", + query.getName(), root); } } } diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ProcedureValidator.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ProcedureValidator.java index d652ecea49..e8be2b01f3 100644 --- a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ProcedureValidator.java +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ProcedureValidator.java @@ -19,6 +19,7 @@ package org.apache.cayenne.project.validation; import java.util.List; +import java.util.function.Supplier; import org.apache.cayenne.map.DataMap; import org.apache.cayenne.map.Procedure; @@ -26,36 +27,37 @@ import org.apache.cayenne.util.Util; import org.apache.cayenne.validation.ValidationResult; -class ProcedureValidator extends ConfigurationNodeValidator { +class ProcedureValidator extends ConfigurationNodeValidator { - void validate(Procedure procedure, ValidationResult validationResult) { - - validateName(procedure, validationResult); + /** + * @param configSupplier the config defining the behavior of this validator. + * @since 5.0 + */ + public ProcedureValidator(Supplier configSupplier) { + super(configSupplier); + } - // check that return value is present - if (procedure.isReturningValue()) { - List parameters = procedure.getCallParameters(); - if (parameters.isEmpty()) { - addFailure( - validationResult, - procedure, - "Procedure '%s' returns a value, but has no parameters", - procedure.getName()); - } - } + @Override + public void validate(Procedure node, ValidationResult validationResult) { + on(node, validationResult) + .performIfEnabled(Inspection.PROCEDURE_NO_NAME, this::checkForName) + .performIfEnabled(Inspection.PROCEDURE_NAME_DUPLICATE, this::checkForDuplicateName) + .performIfEnabled(Inspection.PROCEDURE_NO_PARAMS, this::checkForParams); } - void validateName(Procedure procedure, ValidationResult validationResult) { - String name = procedure.getName(); + private void checkForName(Procedure procedure, ValidationResult validationResult) { // Must have name + String name = procedure.getName(); if (Util.isEmptyString(name)) { addFailure(validationResult, procedure, "Unnamed Procedure"); - return; } + } + private void checkForDuplicateName(Procedure procedure, ValidationResult validationResult) { + String name = procedure.getName(); DataMap map = procedure.getDataMap(); - if (map == null) { + if (map == null || Util.isEmptyString(name)) { return; } @@ -66,13 +68,21 @@ void validateName(Procedure procedure, ValidationResult validationResult) { } if (name.equals(otherProcedure.getName())) { - addFailure( - validationResult, - procedure, - "Duplicate Procedure name: %s", - procedure.getName()); - break; + addFailure(validationResult, procedure, "Duplicate Procedure name: %s", procedure.getName()); + return; } } } + + private void checkForParams(Procedure procedure, ValidationResult validationResult) { + // check that return value is present + if (!procedure.isReturningValue()) { + return; + } + List parameters = procedure.getCallParameters(); + if (parameters.isEmpty()) { + addFailure(validationResult, procedure, "Procedure '%s' returns a value, but has no parameters", + procedure.getName()); + } + } } diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ProjectValidationFailure.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ProjectValidationFailure.java new file mode 100644 index 0000000000..7c5aff4bc6 --- /dev/null +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ProjectValidationFailure.java @@ -0,0 +1,46 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.project.validation; + +import org.apache.cayenne.validation.SimpleValidationFailure; + +/** + * @since 5.0 + */ +public class ProjectValidationFailure extends SimpleValidationFailure { + + private Inspection inspection; + + public ProjectValidationFailure(Object source, Object error) { + super(source, error); + } + + public ProjectValidationFailure(Object source, Object error, Inspection inspection) { + super(source, error); + this.inspection = inspection; + } + + public Inspection getInspection() { + return inspection; + } + + public void setInspection(Inspection inspection) { + this.inspection = inspection; + } +} diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/SQLTemplateValidator.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/SQLTemplateValidator.java index f34113d577..58bcf64d89 100644 --- a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/SQLTemplateValidator.java +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/SQLTemplateValidator.java @@ -19,49 +19,48 @@ package org.apache.cayenne.project.validation; import org.apache.cayenne.map.DataMap; -import org.apache.cayenne.map.QueryDescriptor; import org.apache.cayenne.map.SQLTemplateDescriptor; import org.apache.cayenne.util.Util; import org.apache.cayenne.validation.ValidationResult; import java.util.Map; +import java.util.function.Supplier; -class SQLTemplateValidator extends BaseQueryValidator { +class SQLTemplateValidator extends BaseQueryValidator { - void validate(SQLTemplateDescriptor query, ValidationResult validationResult) { - validateName(query, validationResult); - validateRoot(query, validationResult); - validateDefaultSQL(query, validationResult); - validateCacheGroup(query, validationResult); + /** + * @param configSupplier the config defining the behavior of this validator. + * @since 5.0 + */ + public SQLTemplateValidator(Supplier configSupplier) { + super(configSupplier); } - void validateDefaultSQL(SQLTemplateDescriptor query, ValidationResult validationResult) { - - if (Util.isEmptyString(query.getSql())) { - // see if there is at least one adapter-specific template... - - for (Map.Entry entry : query.getAdapterSql().entrySet()) { - if (!Util.isEmptyString(entry.getValue())) { - return; - } - } - - addFailure( - validationResult, - query, - "SQLTemplate query '%s' has no default SQL template", - query.getName()); - } + @Override + protected ConfigurationNodeValidator.Performer validateQuery( + SQLTemplateDescriptor query, ValidationResult validationResult) { + return super.validateQuery(query, validationResult) + .performIfEnabled(Inspection.SQL_TEMPLATE_NO_ROOT, this::checkForRoot) + .performIfEnabled(Inspection.SQL_TEMPLATE_NO_DEFAULT_SQL, this::checkForDefaultSQL); } - void validateRoot(QueryDescriptor query, ValidationResult validationResult) { + private void checkForRoot(SQLTemplateDescriptor query, ValidationResult validationResult) { DataMap map = query.getDataMap(); if (query.getRoot() == null && map != null) { - addFailure( - validationResult, - query, - "SQLTemplate query '%s' has no root", - query.getName()); + addFailure(validationResult, query, "SQLTemplate query '%s' has no root", query.getName()); + } + } + + private void checkForDefaultSQL(SQLTemplateDescriptor query, ValidationResult validationResult) { + if (!Util.isEmptyString(query.getSql())) { + return; + } + // see if there is at least one adapter-specific template... + for (Map.Entry entry : query.getAdapterSql().entrySet()) { + if (!Util.isEmptyString(entry.getValue())) { + return; + } } + addFailure(validationResult, query, "SQLTemplate query '%s' has no default SQL template", query.getName()); } } diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/SelectQueryValidator.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/SelectQueryValidator.java index 8d8ad578f7..65de0dcc0a 100644 --- a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/SelectQueryValidator.java +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/SelectQueryValidator.java @@ -19,55 +19,63 @@ package org.apache.cayenne.project.validation; import java.util.Iterator; +import java.util.function.Supplier; import org.apache.cayenne.exp.Expression; import org.apache.cayenne.exp.ExpressionException; import org.apache.cayenne.map.DataMap; import org.apache.cayenne.map.Entity; -import org.apache.cayenne.map.QueryDescriptor; import org.apache.cayenne.map.SelectQueryDescriptor; import org.apache.cayenne.query.Ordering; import org.apache.cayenne.util.CayenneMapEntry; import org.apache.cayenne.validation.ValidationResult; -class SelectQueryValidator extends BaseQueryValidator { +class SelectQueryValidator extends BaseQueryValidator { - void validate(SelectQueryDescriptor query, ValidationResult validationResult) { + /** + * @param configSupplier the config defining the behavior of this validator. + * @since 5.0 + */ + public SelectQueryValidator(Supplier configSupplier) { + super(configSupplier); + } - validateName(query, validationResult); + @Override + public void validate(SelectQueryDescriptor node, ValidationResult validationResult) { + ConfigurationNodeValidator.Performer performer = + validateQuery(node, validationResult); - validateCacheGroup(query, validationResult); + performer.performIfEnabled(Inspection.SELECT_QUERY_NO_ROOT, this::checkForRoot); // Resolve root to Entity for further validation - Entity root = validateRoot(query, validationResult); + Entity root = getRootForValidation(node); // validate path-based parts - if (root != null) { - validateQualifier(root, query.getQualifier(), validationResult); - - for (final Ordering ordering : query.getOrderings()) { - validateOrdering(query, root, ordering, validationResult); - } - - if (query.getPrefetchesMap() != null) { - for (String prefetchPath : query.getPrefetchesMap().keySet()) { - validatePrefetch(root, prefetchPath, validationResult); - } - } - + if (root == null) { + return; + } + performer.performIfEnabled(Inspection.SELECT_QUERY_INVALID_QUALIFIER, + () -> validateQualifier(root, node.getQualifier(), validationResult)); + for (final Ordering ordering : node.getOrderings()) { + performer.performIfEnabled(Inspection.SELECT_QUERY_INVALID_ORDERING_PATH, + () -> validateOrdering(node, root, ordering, validationResult)); + } + if (node.getPrefetchesMap() == null) { + return; + } + for (String prefetchPath : node.getPrefetchesMap().keySet()) { + performer.performIfEnabled(Inspection.SELECT_QUERY_INVALID_PREFETCH_PATH, + () -> validatePrefetch(root, prefetchPath, validationResult)); } } - void validatePrefetch(Entity root, String path, ValidationResult validationResult) { + private void validatePrefetch(Entity root, String path, ValidationResult validationResult) { // TODO: andrus 03/10/2010 - should this be implemented? } - void validateOrdering( - QueryDescriptor query, - Entity root, - Ordering ordering, - ValidationResult validationResult) { - + private void validateOrdering(SelectQueryDescriptor query, Entity root, + Ordering ordering, ValidationResult validationResult) { + // validate paths in ordering String path = ordering.getSortSpecString(); Iterator it = root.resolvePathComponents(path); @@ -80,17 +88,20 @@ void validateOrdering( } } - void validateQualifier( - Entity root, - Expression qualifier, - ValidationResult validationResult) { + private void validateQualifier(Entity root, Expression qualifier, ValidationResult validationResult) { // TODO: andrus 03/10/2010 - should this be implemented? } - Entity validateRoot(QueryDescriptor query, ValidationResult validationResult) { + private void checkForRoot(SelectQueryDescriptor query, ValidationResult validationResult) { DataMap map = query.getDataMap(); if (query.getRoot() == null && map != null) { addFailure(validationResult, query, "Query '%s' has no root", query.getName()); + } + } + + private Entity getRootForValidation(SelectQueryDescriptor query) { + DataMap map = query.getDataMap(); + if (query.getRoot() == null && map != null) { return null; } @@ -101,11 +112,11 @@ Entity validateRoot(QueryDescriptor query, ValidationResult validationRes if (map == null) { // maybe standalone entity, otherwise bail... - return (query.getRoot() instanceof Entity) ? (Entity) query.getRoot() : null; + return (query.getRoot() instanceof Entity) ? (Entity) query.getRoot() : null; } if (query.getRoot() instanceof Entity) { - return (Entity) query.getRoot(); + return (Entity) query.getRoot(); } // can't validate Class root - it is likely not accessible from here... @@ -115,9 +126,7 @@ Entity validateRoot(QueryDescriptor query, ValidationResult validationRes // resolve entity if (query.getRoot() instanceof String) { - DataMap parent = query.getDataMap(); - if (parent != null) { return parent.getNamespace().getObjEntity((String) query.getRoot()); } @@ -125,5 +134,4 @@ Entity validateRoot(QueryDescriptor query, ValidationResult validationRes return null; } - } diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ValidationConfig.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ValidationConfig.java new file mode 100644 index 0000000000..d09a71e1e3 --- /dev/null +++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/ValidationConfig.java @@ -0,0 +1,59 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.project.validation; + +import org.apache.cayenne.configuration.DataChannelDescriptor; +import org.apache.cayenne.configuration.xml.DataChannelMetaData; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Optional; +import java.util.Set; + +/** + * @since 5.0 + */ +public class ValidationConfig { + + private final Set enabledInspections; + + public ValidationConfig() { + this(EnumSet.allOf(Inspection.class)); + } + + public ValidationConfig(ValidationConfig other) { + this(other.enabledInspections); + } + + public ValidationConfig(Set enabledInspections) { + this.enabledInspections = Collections.unmodifiableSet(EnumSet.copyOf(enabledInspections)); + } + + public Set getEnabledInspections() { + return enabledInspections; + } + + public boolean isEnabled(Inspection inspection) { + return enabledInspections.contains(inspection); + } + + public static ValidationConfig fromMetadata(DataChannelMetaData metaData, DataChannelDescriptor dataChannel) { + return Optional.ofNullable(metaData.get(dataChannel, ValidationConfig.class)).orElseGet(ValidationConfig::new); + } +} diff --git a/cayenne/src/main/java/org/apache/cayenne/validation/ValidationResult.java b/cayenne/src/main/java/org/apache/cayenne/validation/ValidationResult.java index 15cb0f0678..3d822759af 100644 --- a/cayenne/src/main/java/org/apache/cayenne/validation/ValidationResult.java +++ b/cayenne/src/main/java/org/apache/cayenne/validation/ValidationResult.java @@ -28,14 +28,14 @@ /** * Represents a result of a validation execution. Contains a set of - * {@link ValidationFailure ValidationFailures}that occured in a given context. All + * {@link ValidationFailure ValidationFailures} that occurred in a given context. All * failures are kept in the same order they were added. * * @since 1.1 */ public class ValidationResult implements Serializable { - private List failures; + private final List failures; public ValidationResult() { failures = new ArrayList<>(); @@ -110,7 +110,7 @@ public void clear() { @Override public String toString() { StringBuilder ret = new StringBuilder(); - String separator = System.getProperty("line.separator"); + String separator = System.lineSeparator(); for (ValidationFailure failure : failures) { if (ret.length() > 0) { diff --git a/cayenne/src/main/resources/org/apache/cayenne/schema/11/validation.xsd b/cayenne/src/main/resources/org/apache/cayenne/schema/11/validation.xsd new file mode 100644 index 0000000000..2379dc2357 --- /dev/null +++ b/cayenne/src/main/resources/org/apache/cayenne/schema/11/validation.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/modeler/cayenne-modeler-mac-ext/src/main/java/org/apache/cayenne/modeler/osx/OSXPlatformInitializer.java b/modeler/cayenne-modeler-mac-ext/src/main/java/org/apache/cayenne/modeler/osx/OSXPlatformInitializer.java index ebfa3ced2c..3c1502df9d 100644 --- a/modeler/cayenne-modeler-mac-ext/src/main/java/org/apache/cayenne/modeler/osx/OSXPlatformInitializer.java +++ b/modeler/cayenne-modeler-mac-ext/src/main/java/org/apache/cayenne/modeler/osx/OSXPlatformInitializer.java @@ -83,9 +83,12 @@ private void overrideUIDefaults() { UIManager.put("SplitPane.border", BorderFactory.createEmptyBorder()); UIManager.put("SplitPane.background", darkGrey); UIManager.put("Tree.rendererFillBackground", Boolean.TRUE); + UIManager.put("Tree.paintLines", Boolean.FALSE); UIManager.put("ComboBox.background", Color.WHITE); UIManager.put("ComboBox.selectionBackground", darkGrey); UIManager.put("ComboBox.selectionForeground", Color.BLACK); + UIManager.put("CheckBox.background", Color.WHITE); + UIManager.put("Tree.background", Color.WHITE); UIManager.put("Tree.selectionForeground", Color.BLACK); UIManager.put("Tree.selectionBackground", lightGrey); UIManager.put("Tree.selectionBorderColor", lightGrey); diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/CayenneModelerFrame.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/CayenneModelerFrame.java index ddef81d298..af2fdeec1f 100644 --- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/CayenneModelerFrame.java +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/CayenneModelerFrame.java @@ -54,6 +54,7 @@ import org.apache.cayenne.modeler.action.SaveAction; import org.apache.cayenne.modeler.action.SaveAsAction; import org.apache.cayenne.modeler.action.ShowLogConsoleAction; +import org.apache.cayenne.modeler.action.ShowValidationConfigAction; import org.apache.cayenne.modeler.action.UndoAction; import org.apache.cayenne.modeler.action.ValidateAction; import org.apache.cayenne.modeler.action.dbimport.ReverseEngineeringToolMenuAction; @@ -214,6 +215,7 @@ protected void initMenus() { fileMenu.add(getAction(ExitAction.class).buildMenu()); projectMenu.add(getAction(ValidateAction.class).buildMenu()); + projectMenu.add(getAction(ShowValidationConfigAction.class).buildMenu()); projectMenu.addSeparator(); projectMenu.add(getAction(CreateNodeAction.class).buildMenu()); projectMenu.add(getAction(CreateDataMapAction.class).buildMenu()); @@ -241,7 +243,6 @@ protected void initMenus() { // Menu for opening Log console toolMenu.addSeparator(); - logMenu = getAction(ShowLogConsoleAction.class).buildCheckBoxMenu(); if (!LogConsole.getInstance().getConsoleProperty(LogConsole.DOCKED_PROPERTY) diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ProjectController.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ProjectController.java index aa4291084e..ea34c632c8 100644 --- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ProjectController.java +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ProjectController.java @@ -120,6 +120,8 @@ import org.apache.cayenne.modeler.event.QueryDisplayEvent; import org.apache.cayenne.modeler.event.QueryDisplayListener; import org.apache.cayenne.modeler.event.RelationshipDisplayEvent; +import org.apache.cayenne.modeler.event.ValidationConfigDisplayEvent; +import org.apache.cayenne.modeler.event.ValidationConfigDisplayListener; import org.apache.cayenne.modeler.pref.DataMapDefaults; import org.apache.cayenne.modeler.pref.DataNodeDefaults; import org.apache.cayenne.modeler.pref.ProjectStatePreferences; @@ -595,6 +597,14 @@ public void removeDomainListener(DomainListener listener) { listenerList.remove(DomainListener.class, listener); } + public void addValidationConfigDisplayListener(ValidationConfigDisplayListener listener) { + listenerList.add(ValidationConfigDisplayListener.class, listener); + } + + public void removeValidationConfigDisplayListener(ValidationConfigDisplayListener listener) { + listenerList.remove(ValidationConfigDisplayListener.class, listener); + } + public void addDataNodeDisplayListener(DataNodeDisplayListener listener) { listenerList.add(DataNodeDisplayListener.class, listener); } @@ -772,6 +782,15 @@ public void fireDomainDisplayEvent(DomainDisplayEvent e) { } } + /** + * @since 5.0 + */ + public void fireValidationConfigDisplayEvent(ValidationConfigDisplayEvent event) { + for (ValidationConfigDisplayListener listener : listenerList.getListeners(ValidationConfigDisplayListener.class)) { + listener.validationOptionChanged(event); + } + } + /** * Informs all listeners of the DomainEvent. Does not send the event to its * originator. diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/DefaultActionManager.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/DefaultActionManager.java index 2a9849b1ed..1aec38f2f3 100644 --- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/DefaultActionManager.java +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/DefaultActionManager.java @@ -167,6 +167,11 @@ public DefaultActionManager(@Inject Application application, @Inject Configurati registerAction(new LinkDataMapAction(application)); registerAction(new LinkDataMapsAction(application)); + + registerAction(new ShowValidationConfigAction(application)); + registerAction(new ShowValidationOptionAction(application)); + registerAction(new UpdateValidationConfigAction(application)); + registerAction(new DisableValidationInspectionAction(application)); } private void initActions() { @@ -195,7 +200,12 @@ private void initActions() { GenerateCodeAction.class.getName(), GenerateDBAction.class.getName(), PasteAction.class.getName(), - ReverseEngineeringToolMenuAction.class.getName())); + ReverseEngineeringToolMenuAction.class.getName(), + ShowValidationConfigAction.class.getName(), + ShowValidationOptionAction.class.getName(), + UpdateValidationConfigAction.class.getName(), + DisableValidationInspectionAction.class.getName() + )); DATA_NODE_ACTIONS = new HashSet<>(DOMAIN_ACTIONS); DATA_NODE_ACTIONS.addAll(Arrays.asList( diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/DisableValidationInspectionAction.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/DisableValidationInspectionAction.java new file mode 100644 index 0000000000..b41c4fb0a8 --- /dev/null +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/DisableValidationInspectionAction.java @@ -0,0 +1,70 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.modeler.action; + +import org.apache.cayenne.configuration.DataChannelDescriptor; +import org.apache.cayenne.modeler.Application; +import org.apache.cayenne.project.validation.Inspection; +import org.apache.cayenne.project.validation.ValidationConfig; + +import java.awt.event.ActionEvent; +import java.util.EnumSet; + +/** + * @since 5.0 + */ +public class DisableValidationInspectionAction extends UpdateValidationConfigAction { + + public static final String ACTION_NAME = "Disable inspection"; + + private static final String INSPECTION_PARAM = "inspection"; + + private final ValidateAction validationAction; + + public DisableValidationInspectionAction(Application application) { + super(ACTION_NAME, application); + validationAction = new ValidateAction(application); + + setUndoable(false); + } + + @Override + public void performAction(ActionEvent e) { + Inspection inspection = (Inspection) getValue(INSPECTION_PARAM); + DataChannelDescriptor dataChannel = (DataChannelDescriptor) application.getProject().getRootNode(); + ValidationConfig config = ValidationConfig.fromMetadata(application.getMetaData(), dataChannel); + + EnumSet enabledInspections = EnumSet.copyOf(config.getEnabledInspections()); + enabledInspections.remove(inspection); + putConfig(new ValidationConfig(enabledInspections)); + super.performAction(e); + + validationAction.performAction(e); + } + + public DisableValidationInspectionAction putInspection(Inspection inspection) { + putValue(INSPECTION_PARAM, inspection); + return this; + } + + @Override + public String getIconName() { + return "icon-disable.png"; + } +} diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/ShowValidationConfigAction.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/ShowValidationConfigAction.java new file mode 100644 index 0000000000..6ff0e96c1a --- /dev/null +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/ShowValidationConfigAction.java @@ -0,0 +1,46 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.modeler.action; + +import org.apache.cayenne.configuration.DataChannelDescriptor; +import org.apache.cayenne.modeler.Application; +import org.apache.cayenne.modeler.ProjectController; +import org.apache.cayenne.modeler.event.DomainDisplayEvent; +import org.apache.cayenne.modeler.util.CayenneAction; + +import java.awt.event.ActionEvent; + +/** + * @since 5.0 + */ +public class ShowValidationConfigAction extends CayenneAction { + + public static final String ACTION_NAME = "Show validation config"; + + public ShowValidationConfigAction(Application application) { + super(ACTION_NAME, application); + } + + @Override + public void performAction(ActionEvent e) { + ProjectController controller = getProjectController(); + DataChannelDescriptor dataChannel = (DataChannelDescriptor) controller.getProject().getRootNode(); + controller.fireDomainDisplayEvent(new DomainDisplayEvent(this, dataChannel)); + } +} diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/ShowValidationOptionAction.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/ShowValidationOptionAction.java new file mode 100644 index 0000000000..7a277d9a90 --- /dev/null +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/ShowValidationOptionAction.java @@ -0,0 +1,53 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.modeler.action; + +import org.apache.cayenne.configuration.DataChannelDescriptor; +import org.apache.cayenne.modeler.Application; +import org.apache.cayenne.modeler.ProjectController; +import org.apache.cayenne.modeler.event.ValidationConfigDisplayEvent; +import org.apache.cayenne.project.validation.Inspection; + +import java.awt.event.ActionEvent; + +/** + * @since 5.0 + */ +public class ShowValidationOptionAction extends ShowValidationConfigAction { + + private static final String INSPECTION_PARAM = "inspection"; + + public ShowValidationOptionAction(Application application) { + super(application); + } + + @Override + public void performAction(ActionEvent e) { + super.performAction(e); + Inspection inspection = (Inspection) getValue(INSPECTION_PARAM); + ProjectController controller = getProjectController(); + DataChannelDescriptor dataChannel = (DataChannelDescriptor) controller.getProject().getRootNode(); + controller.fireValidationConfigDisplayEvent(new ValidationConfigDisplayEvent(this, dataChannel, inspection)); + } + + public ShowValidationConfigAction putInspection(Inspection inspection) { + putValue(INSPECTION_PARAM, inspection); + return this; + } +} diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/UpdateValidationConfigAction.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/UpdateValidationConfigAction.java new file mode 100644 index 0000000000..9fe0f10755 --- /dev/null +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/UpdateValidationConfigAction.java @@ -0,0 +1,64 @@ +package org.apache.cayenne.modeler.action; + +import org.apache.cayenne.configuration.DataChannelDescriptor; +import org.apache.cayenne.configuration.event.DomainEvent; +import org.apache.cayenne.configuration.xml.DataChannelMetaData; +import org.apache.cayenne.modeler.Application; +import org.apache.cayenne.modeler.undo.CayenneUndoManager; +import org.apache.cayenne.modeler.undo.UpdateValidationConfigUndoableEdit; +import org.apache.cayenne.modeler.util.CayenneAction; +import org.apache.cayenne.project.validation.ValidationConfig; + +import java.awt.event.ActionEvent; + +/** + * Requires config provided with {@link UpdateValidationConfigAction#putConfig(ValidationConfig)}. + * + * @since 5.0 + * */ +public class UpdateValidationConfigAction extends CayenneAction { + + public static final String ACTION_NAME = "Update ValidationConfig"; + + private static final String CONFIG_PARAM = "config"; + + private boolean undoable; + + public UpdateValidationConfigAction(Application application) { + super(ACTION_NAME, application); + undoable = true; + } + + protected UpdateValidationConfigAction(String name, Application application) { + super(name, application); + } + + public void performAction(Object source) { + performAction(new ActionEvent(source, ActionEvent.ACTION_PERFORMED, null)); + } + + @Override + public void performAction(ActionEvent e) { + DataChannelMetaData metaData = application.getMetaData(); + DataChannelDescriptor dataChannel = ((DataChannelDescriptor) application.getProject().getRootNode()); + ValidationConfig config = (ValidationConfig) getValue(CONFIG_PARAM); + ValidationConfig oldConfig = ValidationConfig.fromMetadata(metaData, dataChannel); + metaData.add(dataChannel, config); + + if (undoable) { + CayenneUndoManager undoManager = application.getUndoManager(); + undoManager.addEdit(new UpdateValidationConfigUndoableEdit(oldConfig, config)); + } + getProjectController().fireDomainEvent(new DomainEvent(e.getSource(), dataChannel)); + } + + public UpdateValidationConfigAction putConfig(ValidationConfig config) { + putValue(CONFIG_PARAM, config); + return this; + } + + public UpdateValidationConfigAction setUndoable(boolean undoable) { + this.undoable = undoable; + return this; + } +} diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/validator/ValidatorDialog.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/validator/ValidatorDialog.java index 2ce67b0e87..04243d24e8 100644 --- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/validator/ValidatorDialog.java +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/validator/ValidatorDialog.java @@ -24,30 +24,30 @@ import com.jgoodies.forms.layout.FormLayout; import org.apache.cayenne.modeler.Application; import org.apache.cayenne.modeler.CayenneModelerFrame; +import org.apache.cayenne.modeler.action.ActionManager; +import org.apache.cayenne.modeler.action.DisableValidationInspectionAction; +import org.apache.cayenne.modeler.action.ShowValidationOptionAction; import org.apache.cayenne.modeler.action.ValidateAction; +import org.apache.cayenne.modeler.event.TablePopupHandler; import org.apache.cayenne.modeler.util.CayenneDialog; +import org.apache.cayenne.project.validation.Inspection; +import org.apache.cayenne.project.validation.ProjectValidationFailure; import org.apache.cayenne.validation.ValidationFailure; -import javax.swing.JButton; -import javax.swing.JDialog; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTable; +import javax.swing.*; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; import javax.swing.table.AbstractTableModel; import javax.swing.table.DefaultTableCellRenderer; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.FlowLayout; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; import java.util.Collections; import java.util.List; /** * Dialog for displaying validation errors. - * */ public class ValidatorDialog extends CayenneDialog { @@ -100,8 +100,15 @@ private void initView() { problemsTable.setRowMargin(3); problemsTable.setCellSelectionEnabled(false); problemsTable.setRowSelectionAllowed(true); + problemsTable.setTableHeader(null); problemsTable.setDefaultRenderer(ValidationFailure.class, new ValidationRenderer()); + ActionManager actionManager = Application.getInstance().getActionManager(); + JPopupMenu popup = new JPopupMenu(); + popup.add(actionManager.getAction(ShowValidationOptionAction.class).buildMenu()); + popup.add(actionManager.getAction(DisableValidationInspectionAction.class).buildMenu()); + TablePopupHandler.install(problemsTable, popup); + // assemble CellConstraints cc = new CellConstraints(); PanelBuilder builder = new PanelBuilder(new FormLayout("fill:200dlu:grow", "pref, 3dlu, fill:40dlu:grow")); @@ -126,9 +133,10 @@ private void initView() { } private void initController() { - setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + problemsTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION); problemsTable.getSelectionModel().addListSelectionListener(e -> showFailedObject()); + problemsTable.getSelectionModel().addListSelectionListener(new ContextMenuSelectionListener()); closeButton.addActionListener(e -> { setVisible(false); @@ -137,18 +145,6 @@ private void initController() { refreshButton.addActionListener(e -> Application.getInstance().getActionManager() .getAction(ValidateAction.class).actionPerformed(e)); - - this.problemsTable.addMouseListener(new MouseAdapter() { - - public void mouseClicked(MouseEvent e) { - int row = problemsTable.rowAtPoint(e.getPoint()); - - // if this happens to be a selected row, re-run object selection - if (row >= 0 && problemsTable.getSelectedRow() == row) { - showFailedObject(); - } - } - }); } protected void refreshFromModel(List list) { @@ -157,11 +153,12 @@ protected void refreshFromModel(List list) { } private void showFailedObject() { - if (problemsTable.getSelectedRow() >= 0) { - ValidationFailure obj = (ValidationFailure) problemsTable.getModel().getValueAt( - problemsTable.getSelectedRow(), 0); - ValidationDisplayHandler.getErrorMsg(obj).displayField(getMediator(), super.getParentEditor()); + if (problemsTable.getSelectedRow() < 0) { + return; } + ValidationFailure failure = (ValidationFailure) problemsTable.getModel() + .getValueAt(problemsTable.getSelectedRow(), 0); + ValidationDisplayHandler.getErrorMsg(failure).displayField(getMediator(), super.getParentEditor()); } class ValidatorTableModel extends AbstractTableModel { @@ -186,16 +183,16 @@ public String getColumnName(int column) { return " "; } - public Class getColumnClass(int columnIndex) { + public Class getColumnClass(int columnIndex) { return ValidationFailure.class; } } // a renderer for the error message - class ValidationRenderer extends DefaultTableCellRenderer { + static class ValidationRenderer extends DefaultTableCellRenderer { public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, - boolean hasFocus, int row, int column) { + boolean hasFocus, int row, int column) { boolean error = false; if (value != null) { @@ -208,4 +205,33 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); } } + + class ContextMenuSelectionListener implements ListSelectionListener { + + @Override + public void valueChanged(ListSelectionEvent e) { + int index = problemsTable.getSelectedRow(); + if (e.getValueIsAdjusting() || index < 0) { + // not valid + setActionsFor(null); + return; + } + + ValidationFailure failure = (ValidationFailure) problemsTable.getModel().getValueAt(index, 0); + Inspection inspection = failure instanceof ProjectValidationFailure + ? ((ProjectValidationFailure) failure).getInspection() + : null; + setActionsFor(inspection); + } + + private void setActionsFor(Inspection inspection) { + ActionManager actionManager = Application.getInstance().getActionManager(); + actionManager.getAction(DisableValidationInspectionAction.class) + .putInspection(inspection) + .setEnabled(inspection != null); + actionManager.getAction(ShowValidationOptionAction.class) + .putInspection(inspection); + } + + } } diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/DataDomainTabbedView.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/DataDomainTabbedView.java index 7d1f442db3..28db1f7585 100644 --- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/DataDomainTabbedView.java +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/DataDomainTabbedView.java @@ -20,35 +20,38 @@ import org.apache.cayenne.modeler.ProjectController; import org.apache.cayenne.modeler.action.GenerateCodeAction; +import org.apache.cayenne.modeler.action.ShowValidationConfigAction; import org.apache.cayenne.modeler.action.dbimport.ReverseEngineeringToolMenuAction; import org.apache.cayenne.modeler.editor.cgen.domain.CgenTabController; import org.apache.cayenne.modeler.editor.dbimport.domain.DbImportTabController; +import org.apache.cayenne.modeler.editor.validation.ValidationTabController; import org.apache.cayenne.modeler.event.DomainDisplayEvent; import org.apache.cayenne.modeler.event.DomainDisplayListener; import org.apache.cayenne.modeler.event.EntityDisplayEvent; import org.apache.cayenne.modeler.graph.DataDomainGraphTab; -import javax.swing.JScrollPane; -import javax.swing.JTabbedPane; +import javax.swing.*; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; /** - * DataDomain editing tabs container + * DataDomain editing tabs container */ -public class DataDomainTabbedView extends JTabbedPane - implements ChangeListener, DomainDisplayListener { - +public class DataDomainTabbedView extends JTabbedPane implements ChangeListener, DomainDisplayListener { + ProjectController mediator; - + DataDomainGraphTab graphTab; - private JScrollPane cgenView; + private JComponent cgenView; private CgenTabController cgenTabController; - private JScrollPane dbImportView; + private JComponent dbImportView; private DbImportTabController dbImportTabController; + private JComponent validationTabView; + private ValidationTabController validationTabController; /** * constructor + * * @param mediator mediator instance */ public DataDomainTabbedView(ProjectController mediator) { @@ -61,7 +64,7 @@ public DataDomainTabbedView(ProjectController mediator) { * create tabs */ private void initView() { - + setTabPlacement(JTabbedPane.TOP); // add panels to tabs @@ -83,15 +86,21 @@ private void initView() { graphTab = new DataDomainGraphTab(mediator); addTab("Graph", graphTab); + + validationTabController = new ValidationTabController(mediator); + validationTabView = validationTabController.getView(); + addTab("Validation", validationTabView); } public void stateChanged(ChangeEvent e) { if (getSelectedComponent() == graphTab) { graphTab.refresh(); - } else if(getSelectedComponent() == cgenView) { + } else if (getSelectedComponent() == cgenView) { cgenTabController.getView().initView(); - } else if(getSelectedComponent() == dbImportView) { + } else if (getSelectedComponent() == dbImportView) { dbImportTabController.getView().initView(); + } else if (getSelectedComponent() == validationTabView) { + validationTabController.getView().initView(); } } @@ -100,17 +109,20 @@ public void currentDomainChanged(DomainDisplayEvent e) { //need select an entity setSelectedComponent(graphTab); } - if(getSelectedComponent() == cgenView) { + if (getSelectedComponent() == cgenView) { fireStateChanged(); } - if(e.getSource() instanceof GenerateCodeAction) { + if (e.getSource() instanceof GenerateCodeAction) { setSelectedComponent(cgenView); } - if(getSelectedComponent() == dbImportView) { + if (getSelectedComponent() == dbImportView) { fireStateChanged(); } - if(e.getSource() instanceof ReverseEngineeringToolMenuAction) { + if (e.getSource() instanceof ReverseEngineeringToolMenuAction) { setSelectedComponent(dbImportView); } + if (e.getSource() instanceof ShowValidationConfigAction) { + setSelectedComponent(validationTabView); + } } } diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/validation/InspectionCheckBoxTree.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/validation/InspectionCheckBoxTree.java new file mode 100644 index 0000000000..29926cd184 --- /dev/null +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/validation/InspectionCheckBoxTree.java @@ -0,0 +1,127 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.modeler.editor.validation; + +import org.apache.cayenne.project.validation.Inspection; +import org.apache.cayenne.swing.components.tree.ChangeOptimizingTreeModel; +import org.apache.cayenne.swing.components.tree.CheckBoxNodeData; +import org.apache.cayenne.swing.components.tree.CheckBoxTree; + +import javax.swing.*; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.MutableTreeNode; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionAdapter; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +class InspectionCheckBoxTree extends CheckBoxTree { + + private final Map inspectionPathCache; + + private InspectionCheckBoxTree(TreeModel model, Map inspectionPathCache) { + super(model); + this.inspectionPathCache = inspectionPathCache; + + labelTree.addMouseMotionListener(new ToolTipHandler(labelTree)); + checkBoxTree.addMouseMotionListener(new ToolTipHandler(checkBoxTree)); + } + + public void refreshEnabledInspections(Set enabledInspections) { + TreeModel model = getModel(); + for (var inspectionPath : inspectionPathCache.entrySet()) { + Inspection inspection = inspectionPath.getKey(); + TreePath path = inspectionPath.getValue(); + DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); + CheckBoxNodeData data = (CheckBoxNodeData) node.getUserObject(); + + model.valueForPathChanged(path, data.withState(enabledInspections.contains(inspection))); + } + } + + public void selectInspection(Inspection inspection) { + getSelectionModel().setSelectionPath(inspectionPathCache.get(inspection)); + } + + public static InspectionCheckBoxTree build() { + Map inspectionPathCache = new HashMap<>(); + DefaultMutableTreeNode root = new DefaultMutableTreeNode(); + Map groupNodes = new EnumMap<>(Inspection.Group.class); + for (Inspection inspection : Inspection.values()) { + MutableTreeNode groupNode = groupNodes.computeIfAbsent(inspection.group(), g -> addChild(root, g)); + DefaultMutableTreeNode inspectionNode = addChild(groupNode, inspection); + inspectionPathCache.put(inspection, new TreePath(inspectionNode.getPath())); + } + + ChangeOptimizingTreeModel model = new ChangeOptimizingTreeModel(root); + return new InspectionCheckBoxTree(model, inspectionPathCache); + } + + private static DefaultMutableTreeNode addChild(MutableTreeNode parent, Object value) { + DefaultMutableTreeNode node = new DefaultMutableTreeNode(new CheckBoxNodeData(value, false)); + parent.insert(node, parent.getChildCount()); + return node; + } + + private static class ToolTipHandler extends MouseMotionAdapter { + + private final JTree tree; + private Object latestTarget; + + public ToolTipHandler(JTree tree) { + this.tree = tree; + ToolTipManager.sharedInstance().registerComponent(tree); + } + + @Override + public void mouseMoved(MouseEvent e) { + ToolTipManager toolTipManager = ToolTipManager.sharedInstance(); + TreePath closestPath = tree.getClosestPathForLocation(e.getX(), e.getY()); + if (closestPath == null || !(closestPath.getLastPathComponent() instanceof DefaultMutableTreeNode)) { + tree.setToolTipText(null); + toolTipManager.mousePressed(e); // this will hide tooltip no matter the event + return; + } + + DefaultMutableTreeNode component = (DefaultMutableTreeNode) closestPath.getLastPathComponent(); + if (component != latestTarget) { + toolTipManager.mousePressed(e); // this will hide tooltip no matter the event + latestTarget = component; + } + if (!(component.getUserObject() instanceof CheckBoxNodeData)) { + tree.setToolTipText(null); + return; + } + + CheckBoxNodeData data = (CheckBoxNodeData) component.getUserObject(); + if (data.getValue() instanceof Inspection) { + String description = ((Inspection) data.getValue()).description(); + tree.setToolTipText(description.endsWith(".") ? description : description + "."); + } else if (data.getValue() instanceof Inspection.Group) { + tree.setToolTipText(((Inspection.Group) data.getValue()).readableName() + " inspections."); + } else { + tree.setToolTipText(null); + } + } + } +} diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/validation/ObjAttributeWrapperValidator.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/validation/ObjAttributeWrapperValidator.java index 1b4f347d61..29ca9ae1d1 100644 --- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/validation/ObjAttributeWrapperValidator.java +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/validation/ObjAttributeWrapperValidator.java @@ -22,28 +22,39 @@ import org.apache.cayenne.modeler.editor.ObjAttributeTableModel; import org.apache.cayenne.modeler.editor.wrapper.ObjAttributeWrapper; import org.apache.cayenne.project.validation.ConfigurationNodeValidator; +import org.apache.cayenne.project.validation.ValidationConfig; import org.apache.cayenne.validation.ValidationResult; -public class ObjAttributeWrapperValidator extends ConfigurationNodeValidator { +import java.util.function.Supplier; - public boolean validate(ObjAttributeWrapper wrapper, ValidationResult validationResult) { +public class ObjAttributeWrapperValidator extends ConfigurationNodeValidator { + + /** + * @param configSupplier the config defining the behavior of this validator. + * @since 5.0 + */ + public ObjAttributeWrapperValidator(Supplier configSupplier) { + super(configSupplier); + } + + @Override + public void validate(ObjAttributeWrapper node, ValidationResult validationResult) { + validateName(node, validationResult); + } + + private void validateName(ObjAttributeWrapper wrapper, ValidationResult validationResult) { if (isAttributeNameOverlapped(wrapper)) { addFailure(validationResult, new AttributeValidationFailure( ObjAttributeTableModel.OBJ_ATTRIBUTE, "Duplicate attribute name.")); } - - return wrapper.isValid(); } /** - * @return false if entity has attribute with the same name. + * @return true if entity has attribute with the same name. */ - private boolean isAttributeNameOverlapped(ObjAttributeWrapper attr) { - ObjAttribute temp = attr.getEntity().getAttributeMap().get(attr.getName()); - if (temp != null && attr.getValue() != temp ){ - return true; - } - return false; + private boolean isAttributeNameOverlapped(ObjAttributeWrapper wrapper) { + ObjAttribute otherAttribute = wrapper.getEntity().getAttributeMap().get(wrapper.getName()); + return otherAttribute != null && wrapper.getValue() != otherAttribute; } } diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/validation/ValidationTab.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/validation/ValidationTab.java new file mode 100644 index 0000000000..ee27627d62 --- /dev/null +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/validation/ValidationTab.java @@ -0,0 +1,48 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.modeler.editor.validation; + +import javax.swing.*; +import java.awt.*; + +/** + * @since 5.0 + */ +public class ValidationTab extends JPanel { + + final ValidationTabController controller; + InspectionCheckBoxTree inspectionTree; + + public ValidationTab(ValidationTabController controller) { + this.controller = controller; + } + + public void initView() { + removeAll(); + + inspectionTree = InspectionCheckBoxTree.build(); + JScrollPane scrollableInspectionPane = new JScrollPane(inspectionTree); + scrollableInspectionPane.getVerticalScrollBar().setUnitIncrement(12); + + setLayout(new BorderLayout()); + add(scrollableInspectionPane, BorderLayout.CENTER); + + controller.onViewLoaded(); + } +} diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/validation/ValidationTabController.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/validation/ValidationTabController.java new file mode 100644 index 0000000000..88a2b84a56 --- /dev/null +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/validation/ValidationTabController.java @@ -0,0 +1,150 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.modeler.editor.validation; + +import org.apache.cayenne.configuration.event.DomainEvent; +import org.apache.cayenne.configuration.event.DomainListener; +import org.apache.cayenne.configuration.xml.DataChannelMetaData; +import org.apache.cayenne.modeler.ProjectController; +import org.apache.cayenne.modeler.action.UpdateValidationConfigAction; +import org.apache.cayenne.modeler.event.ValidationConfigDisplayEvent; +import org.apache.cayenne.modeler.event.ValidationConfigDisplayListener; +import org.apache.cayenne.project.validation.Inspection; +import org.apache.cayenne.project.validation.ValidationConfig; +import org.apache.cayenne.swing.components.tree.CheckBoxNodeData; + +import javax.swing.event.TreeModelEvent; +import javax.swing.event.TreeModelListener; +import javax.swing.tree.DefaultMutableTreeNode; +import java.util.EnumSet; +import java.util.Set; + +/** + * @since 5.0 + */ +public class ValidationTabController implements DomainListener, ValidationConfigDisplayListener { + + private final ProjectController projectController; + private final DataChannelMetaData metaData; + private final ValidationTab view; + + private Set enabledInspections; + private InspectionTreeModelListener inspectionTreeModelListener; + + public ValidationTabController(ProjectController projectController) { + this.projectController = projectController; + this.metaData = projectController.getApplication().getInjector().getInstance(DataChannelMetaData.class); + this.view = new ValidationTab(this); + } + + @Override + public void domainChanged(DomainEvent e) { + // get rid of reverberated config updates + view.inspectionTree.getModel().removeTreeModelListener(inspectionTreeModelListener); + configUpdated(ValidationConfig.fromMetadata(metaData, e.getDomain())); + view.inspectionTree.getModel().addTreeModelListener(inspectionTreeModelListener); + } + + @Override + public void validationOptionChanged(ValidationConfigDisplayEvent event) { + Inspection inspection = event.getInspection(); + if (inspection != null) { + view.inspectionTree.selectInspection(inspection); + } + } + + public ValidationTab getView() { + return view; + } + + void onViewLoaded() { + projectController.addDomainListener(this); + projectController.addValidationConfigDisplayListener(this); + configUpdated(ValidationConfig.fromMetadata(metaData, projectController.getCurrentDataChanel())); + initListeners(); + } + + private void initListeners() { + inspectionTreeModelListener = new InspectionTreeModelListener(); + view.inspectionTree.getModel().addTreeModelListener(inspectionTreeModelListener); + } + + private void configUpdated(ValidationConfig updatedConfig) { + Set configInspections = updatedConfig.getEnabledInspections(); + enabledInspections = configInspections.isEmpty() + ? EnumSet.noneOf(Inspection.class) + : EnumSet.copyOf(configInspections); + view.inspectionTree.refreshEnabledInspections(enabledInspections); + } + + private class InspectionTreeModelListener implements TreeModelListener { + + @Override + public void treeNodesChanged(TreeModelEvent e) { + handleEvent(e); + } + + @Override + public void treeNodesInserted(TreeModelEvent e) { + handleEvent(e); + } + + @Override + public void treeNodesRemoved(TreeModelEvent e) { + handleEvent(e); + } + + @Override + public void treeStructureChanged(TreeModelEvent e) { + handleEvent(e); + } + + private void handleEvent(TreeModelEvent e) { + boolean inspectionsUpdated = false; + for (Object child : e.getChildren()) { + if (!(child instanceof DefaultMutableTreeNode)) { + continue; + } + DefaultMutableTreeNode node = (DefaultMutableTreeNode) child; + if (!(node.getUserObject() instanceof CheckBoxNodeData)) { + continue; + } + CheckBoxNodeData data = (CheckBoxNodeData) node.getUserObject(); + if (!(data.getValue() instanceof Inspection)) { + continue; + } + Inspection inspection = (Inspection) data.getValue(); + + if (data.isSelected()) { + inspectionsUpdated = enabledInspections.add(inspection); + } else { + inspectionsUpdated = enabledInspections.remove(inspection); + } + } + if (inspectionsUpdated) { + projectController.getApplication() + .getActionManager() + .getAction(UpdateValidationConfigAction.class) + .putConfig(new ValidationConfig(enabledInspections)) + .setUndoable(true) + .performAction(this); + } + } + } +} diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/wrapper/ObjAttributeWrapper.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/wrapper/ObjAttributeWrapper.java index f3d723d184..f7ce151a07 100644 --- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/wrapper/ObjAttributeWrapper.java +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/wrapper/ObjAttributeWrapper.java @@ -20,12 +20,15 @@ import java.util.List; +import org.apache.cayenne.configuration.ConfigurationNode; +import org.apache.cayenne.configuration.ConfigurationNodeVisitor; import org.apache.cayenne.exp.ExpressionException; import org.apache.cayenne.map.DbAttribute; import org.apache.cayenne.map.ObjAttribute; import org.apache.cayenne.map.ObjEntity; import org.apache.cayenne.modeler.editor.validation.ObjAttributeWrapperValidator; import org.apache.cayenne.modeler.util.ProjectUtil; +import org.apache.cayenne.project.validation.ValidationConfig; import org.apache.cayenne.validation.ValidationFailure; import org.apache.cayenne.validation.ValidationResult; @@ -33,12 +36,12 @@ * A wrapper for a ObjAttribute instance. Allows to add failures, that connected * with attribute. */ -public class ObjAttributeWrapper implements Wrapper { +public class ObjAttributeWrapper implements Wrapper, ConfigurationNode { private final ObjAttribute objAttribute; private final ValidationResult validationResult; - private final ObjAttributeWrapperValidator validator = new ObjAttributeWrapperValidator(); + private final ObjAttributeWrapperValidator validator = new ObjAttributeWrapperValidator(ValidationConfig::new); // TODO: for now name is only wrapped attribute we validating but this // can be extended to other ObjAttribute fields as well @@ -170,4 +173,8 @@ public String getDbAttributePath() { return objAttribute.getDbAttributePath().value(); } + @Override + public T acceptVisitor(ConfigurationNodeVisitor visitor) { + return visitor.visitObjAttribute(objAttribute); + } } diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/event/TablePopupHandler.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/event/TablePopupHandler.java index b6e9955c9f..92963bec43 100644 --- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/event/TablePopupHandler.java +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/event/TablePopupHandler.java @@ -21,9 +21,8 @@ import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; -import javax.swing.JPopupMenu; - -import org.apache.cayenne.modeler.util.CayenneTable; +import javax.swing.*; +import javax.swing.event.ChangeEvent; /** * A class to handle mouse right-click on table and show popup after selecting specified @@ -31,14 +30,14 @@ */ public class TablePopupHandler extends MouseAdapter { - private final CayenneTable table; + private final JTable table; private final JPopupMenu popup; /** * Creates new mouse handler for table, which shows specified popupmenu on right-click */ - public TablePopupHandler(CayenneTable table, JPopupMenu popup) { + public TablePopupHandler(JTable table, JPopupMenu popup) { this.table = table; this.popup = popup; } @@ -46,7 +45,7 @@ public TablePopupHandler(CayenneTable table, JPopupMenu popup) { /** * Creates and installs mouse listener for a table */ - public static void install(CayenneTable table, JPopupMenu popup) { + public static void install(JTable table, JPopupMenu popup) { table.addMouseListener(new TablePopupHandler(table, popup)); } @@ -63,15 +62,16 @@ public void mousePressed(MouseEvent e) { */ @Override public void mouseReleased(MouseEvent e) { - if (e.isPopupTrigger()) { - table.cancelEditing(); - - int row = table.rowAtPoint(e.getPoint()); - if (row != -1 && !table.getSelectionModel().isSelectedIndex(row)) { - table.setRowSelectionInterval(row, row); - } + if (!e.isPopupTrigger()) { + return; + } + table.editingCanceled(new ChangeEvent(this)); - popup.show(table, e.getX(), e.getY()); + int row = table.rowAtPoint(e.getPoint()); + if (row != -1 && !table.getSelectionModel().isSelectedIndex(row)) { + table.setRowSelectionInterval(row, row); } + + popup.show(table, e.getX(), e.getY()); } } diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/event/ValidationConfigDisplayEvent.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/event/ValidationConfigDisplayEvent.java new file mode 100644 index 0000000000..d14c1a0a6c --- /dev/null +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/event/ValidationConfigDisplayEvent.java @@ -0,0 +1,47 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.modeler.event; + +import org.apache.cayenne.configuration.DataChannelDescriptor; +import org.apache.cayenne.project.validation.Inspection; + +/** + * @since 5.0 + */ +public class ValidationConfigDisplayEvent extends DomainDisplayEvent { + + private Inspection inspection; + + public ValidationConfigDisplayEvent(Object src, DataChannelDescriptor domain) { + super(src, domain); + } + + public ValidationConfigDisplayEvent(Object src, DataChannelDescriptor domain, Inspection inspection) { + super(src, domain); + this.inspection = inspection; + } + + public Inspection getInspection() { + return inspection; + } + + public void setInspection(Inspection inspection) { + this.inspection = inspection; + } +} diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/event/ValidationConfigDisplayListener.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/event/ValidationConfigDisplayListener.java new file mode 100644 index 0000000000..b6481fd89a --- /dev/null +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/event/ValidationConfigDisplayListener.java @@ -0,0 +1,29 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.modeler.event; + +import java.util.EventListener; + +/** + * @since 5.0 + */ +public interface ValidationConfigDisplayListener extends EventListener { + + void validationOptionChanged(ValidationConfigDisplayEvent event); +} diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/init/CayenneModelerModule.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/init/CayenneModelerModule.java index 87716e3800..0e9e9f68e5 100644 --- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/init/CayenneModelerModule.java +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/init/CayenneModelerModule.java @@ -34,9 +34,12 @@ import org.apache.cayenne.modeler.init.platform.PlatformInitializer; import org.apache.cayenne.modeler.util.DefaultWidgetFactory; import org.apache.cayenne.modeler.util.WidgetFactory; +import org.apache.cayenne.modeler.validation.ConfigurableProjectValidator; +import org.apache.cayenne.modeler.validation.extension.ValidationExtension; import org.apache.cayenne.project.ProjectModule; import org.apache.cayenne.project.extension.ExtensionAwareHandlerFactory; import org.apache.cayenne.project.extension.info.InfoExtension; +import org.apache.cayenne.project.validation.ProjectValidator; import org.xml.sax.XMLReader; /** @@ -58,6 +61,9 @@ public void configure(Binder binder) { .addExtension(InfoExtension.class) .addExtension(GraphExtension.class) .addExtension(DbImportExtension.class) - .addExtension(CgenExtension.class); + .addExtension(CgenExtension.class) + .addExtension(ValidationExtension.class); + + binder.bind(ProjectValidator.class).to(ConfigurableProjectValidator.class); } } diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/undo/UpdateValidationConfigUndoableEdit.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/undo/UpdateValidationConfigUndoableEdit.java new file mode 100644 index 0000000000..67f66e624a --- /dev/null +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/undo/UpdateValidationConfigUndoableEdit.java @@ -0,0 +1,60 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.modeler.undo; + +import org.apache.cayenne.modeler.action.UpdateValidationConfigAction; +import org.apache.cayenne.project.validation.ValidationConfig; + +import javax.swing.undo.CannotRedoException; +import javax.swing.undo.CannotUndoException; + +/** + * @since 5.0 + */ +public class UpdateValidationConfigUndoableEdit extends CayenneUndoableEdit { + + private final ValidationConfig oldConfig; + private final ValidationConfig newConfig; + + public UpdateValidationConfigUndoableEdit(ValidationConfig oldConfig, ValidationConfig newConfig) { + this.oldConfig = oldConfig; + this.newConfig = newConfig; + } + + @Override + public String getPresentationName() { + return "Update ValidationConfig"; + } + + @Override + public void redo() throws CannotRedoException { + actionManager.getAction(UpdateValidationConfigAction.class) + .putConfig(newConfig) + .setUndoable(false) + .performAction(this); + } + + @Override + public void undo() throws CannotUndoException { + actionManager.getAction(UpdateValidationConfigAction.class) + .putConfig(oldConfig) + .setUndoable(false) + .performAction(this); + } +} diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/util/CayenneAction.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/util/CayenneAction.java index deca27abfa..a893a9f2a9 100644 --- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/util/CayenneAction.java +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/util/CayenneAction.java @@ -21,8 +21,6 @@ package org.apache.cayenne.modeler.util; import java.awt.event.ActionEvent; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; import javax.swing.AbstractAction; import javax.swing.Action; @@ -115,7 +113,7 @@ public KeyStroke getAcceleratorKey() { } /** - * Returns the name of the icon that should be used for buttons. Name will be reolved + * Returns the name of the icon that should be used for buttons. Name will be resolved * relative to RESOURCE_PATH. Default implementation returns * null. */ @@ -254,7 +252,7 @@ public static final class CayenneToolbarButton extends JButton { static private final String[] POSITIONS = {"only", "first", "middle", "last"}; - protected boolean showingText; + private boolean showingText; /** * Constructor for CayenneMenuItem. diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/validation/ConfigurableProjectValidator.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/validation/ConfigurableProjectValidator.java new file mode 100644 index 0000000000..fadc82e8b9 --- /dev/null +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/validation/ConfigurableProjectValidator.java @@ -0,0 +1,40 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.modeler.validation; + +import org.apache.cayenne.configuration.DataChannelDescriptor; +import org.apache.cayenne.configuration.xml.DataChannelMetaData; +import org.apache.cayenne.di.Inject; +import org.apache.cayenne.modeler.Application; +import org.apache.cayenne.project.validation.DefaultProjectValidator; +import org.apache.cayenne.project.validation.ValidationConfig; + +/** + * @since 5.0 + */ +public class ConfigurableProjectValidator extends DefaultProjectValidator { + + public ConfigurableProjectValidator(@Inject Application application) { + super(() -> { + DataChannelMetaData metaData = application.getMetaData(); + DataChannelDescriptor dataChannel = (DataChannelDescriptor) application.getProject().getRootNode(); + return ValidationConfig.fromMetadata(metaData, dataChannel); + }); + } +} diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/validation/extension/ValidationConfigHandler.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/validation/extension/ValidationConfigHandler.java new file mode 100644 index 0000000000..d5a30a3ffd --- /dev/null +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/validation/extension/ValidationConfigHandler.java @@ -0,0 +1,80 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.modeler.validation.extension; + +import org.apache.cayenne.configuration.xml.DataChannelMetaData; +import org.apache.cayenne.configuration.xml.NamespaceAwareNestedTagHandler; +import org.apache.cayenne.project.validation.Inspection; +import org.apache.cayenne.project.validation.ValidationConfig; +import org.xml.sax.Attributes; + +import java.util.EnumSet; +import java.util.Set; + +/** + * @since 5.0 + */ +public class ValidationConfigHandler extends NamespaceAwareNestedTagHandler { + + static final String CONFIG_TAG = "validation"; + + static final String EXCLUDE_TAG = "exclude"; + + private final DataChannelMetaData metaData; + private final EnumSet disabledInspections; + + ValidationConfigHandler(NamespaceAwareNestedTagHandler parentHandler, DataChannelMetaData metaData) { + super(parentHandler); + this.metaData = metaData; + targetNamespace = ValidationExtension.NAMESPACE; + disabledInspections = EnumSet.noneOf(Inspection.class); + } + + @Override + protected boolean processElement(String namespaceURI, String localName, Attributes attributes) { + if (CONFIG_TAG.equals(localName)) { + disabledInspections.clear(); + return true; + } + return false; + } + + @Override + protected boolean processCharData(String localName, String data) { + if (localName.equals(EXCLUDE_TAG)) { + disabledInspections.add(Inspection.valueOf(data)); + return true; + } + return false; + } + + @Override + public void endElement(String namespaceURI, String localName, String qName) { + if (CONFIG_TAG.equals(localName)) { + createConfig(); + } + } + + private void createConfig() { + Set enabledInspections = EnumSet.complementOf(disabledInspections); + loaderContext.addDataChannelListener(dataChannel -> { + metaData.add(dataChannel, new ValidationConfig(enabledInspections)); + }); + } +} diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/validation/extension/ValidationExtension.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/validation/extension/ValidationExtension.java new file mode 100644 index 0000000000..f78cb023d4 --- /dev/null +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/validation/extension/ValidationExtension.java @@ -0,0 +1,54 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.modeler.validation.extension; + +import org.apache.cayenne.configuration.ConfigurationNodeVisitor; +import org.apache.cayenne.configuration.xml.DataChannelMetaData; +import org.apache.cayenne.di.Inject; +import org.apache.cayenne.project.Project; +import org.apache.cayenne.project.extension.BaseNamingDelegate; +import org.apache.cayenne.project.extension.LoaderDelegate; +import org.apache.cayenne.project.extension.ProjectExtension; +import org.apache.cayenne.project.extension.SaverDelegate; + +/** + * @since 5.0 + */ +public class ValidationExtension implements ProjectExtension { + + static final String NAMESPACE = "http://cayenne.apache.org/schema/" + Project.VERSION + "/validation"; + + @Inject + protected DataChannelMetaData metadata; + + @Override + public LoaderDelegate createLoaderDelegate() { + return new ValidationLoaderDelegate(metadata); + } + + @Override + public SaverDelegate createSaverDelegate() { + return new ValidationSaverDelegate(metadata); + } + + @Override + public ConfigurationNodeVisitor createNamingDelegate() { + return new BaseNamingDelegate(); + } +} diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/validation/extension/ValidationLoaderDelegate.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/validation/extension/ValidationLoaderDelegate.java new file mode 100644 index 0000000000..f2c55cdad2 --- /dev/null +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/validation/extension/ValidationLoaderDelegate.java @@ -0,0 +1,49 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.modeler.validation.extension; + +import org.apache.cayenne.configuration.xml.DataChannelMetaData; +import org.apache.cayenne.configuration.xml.NamespaceAwareNestedTagHandler; +import org.apache.cayenne.di.Inject; +import org.apache.cayenne.project.extension.LoaderDelegate; + +/** + * @since 5.0 + */ +public class ValidationLoaderDelegate implements LoaderDelegate { + + private final DataChannelMetaData metaData; + + ValidationLoaderDelegate(@Inject DataChannelMetaData metaData) { + this.metaData = metaData; + } + + @Override + public String getTargetNamespace() { + return ValidationExtension.NAMESPACE; + } + + @Override + public NamespaceAwareNestedTagHandler createHandler(NamespaceAwareNestedTagHandler parent, String tag) { + if (ValidationConfigHandler.CONFIG_TAG.equals(tag)) { + return new ValidationConfigHandler(parent, metaData); + } + return null; + } +} diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/validation/extension/ValidationSaverDelegate.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/validation/extension/ValidationSaverDelegate.java new file mode 100644 index 0000000000..840cc697f7 --- /dev/null +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/validation/extension/ValidationSaverDelegate.java @@ -0,0 +1,61 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.modeler.validation.extension; + +import org.apache.cayenne.configuration.DataChannelDescriptor; +import org.apache.cayenne.configuration.xml.DataChannelMetaData; +import org.apache.cayenne.project.extension.BaseSaverDelegate; +import org.apache.cayenne.project.validation.Inspection; +import org.apache.cayenne.project.validation.ValidationConfig; + +import java.util.EnumSet; +import java.util.Set; + +/** + * @since 5.0 + */ +public class ValidationSaverDelegate extends BaseSaverDelegate { + + private final DataChannelMetaData metaData; + + ValidationSaverDelegate(DataChannelMetaData metaData) { + this.metaData = metaData; + } + + @Override + public Void visitDataChannelDescriptor(DataChannelDescriptor channelDescriptor) { + return printValidationConfig(channelDescriptor); + } + + private Void printValidationConfig(DataChannelDescriptor dataChannelDescriptor) { + ValidationConfig validationConfig = ValidationConfig.fromMetadata(metaData, dataChannelDescriptor); + Set disabledInspections = EnumSet.allOf(Inspection.class); + disabledInspections.removeAll(validationConfig.getEnabledInspections()); + if (disabledInspections.isEmpty()) { + return null; + } + + encoder.start("validation").attribute("xmlns", ValidationExtension.NAMESPACE); + for (Inspection inspection : disabledInspections) { + encoder.simpleTag(ValidationConfigHandler.EXCLUDE_TAG, inspection.name()); + } + encoder.end(); + return null; + } +} diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/swing/components/tree/ChangeOptimizingTreeModel.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/swing/components/tree/ChangeOptimizingTreeModel.java new file mode 100644 index 0000000000..8ec6c1ad96 --- /dev/null +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/swing/components/tree/ChangeOptimizingTreeModel.java @@ -0,0 +1,51 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.swing.components.tree; + +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; +import java.util.Objects; + +/** + * @since 5.0 + */ +public class ChangeOptimizingTreeModel extends DefaultTreeModel { + + public ChangeOptimizingTreeModel(TreeNode root) { + super(root); + } + + public ChangeOptimizingTreeModel(TreeNode root, boolean asksAllowsChildren) { + super(root, asksAllowsChildren); + } + + @Override + public void valueForPathChanged(TreePath path, Object newValue) { + if (path.getLastPathComponent() instanceof DefaultMutableTreeNode) { + Object value = ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject(); + if (!Objects.equals(value, newValue)) { + super.valueForPathChanged(path, newValue); + } + } else { + super.valueForPathChanged(path, newValue); + } + } +} diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/swing/components/tree/CheckBoxNodeData.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/swing/components/tree/CheckBoxNodeData.java new file mode 100644 index 0000000000..c1bc3a832e --- /dev/null +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/swing/components/tree/CheckBoxNodeData.java @@ -0,0 +1,114 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.swing.components.tree; + +import java.util.Objects; + +/** + * @since 5.0 + */ +public class CheckBoxNodeData { + + protected final Object value; + protected final State state; + + public CheckBoxNodeData(Object value, boolean isSelected) { + this(value, isSelected ? State.SELECTED : State.DESELECTED); + } + + public CheckBoxNodeData(CheckBoxNodeData data) { + this(data.value, data.state); + } + + public CheckBoxNodeData(Object value, State state) { + this.value = value; + this.state = Objects.requireNonNull(state); + } + + protected CheckBoxNodeData(CheckBoxNodeData data, State state) { + this(data.value, state); + } + + public CheckBoxNodeData toggleState() { + switch (state) { + case DESELECTED: + case INDETERMINATE: + return withState(State.SELECTED); + case SELECTED: + return withState(State.DESELECTED); + default: + throw new IllegalStateException(); + } + } + + public CheckBoxNodeData withState(State state) { + return new CheckBoxNodeData(this, state); + } + + public CheckBoxNodeData withState(boolean isSelected) { + return isSelected ? withState(State.SELECTED) : withState(State.DESELECTED); + } + + public boolean isSelected() { + return getState() == State.SELECTED; + } + + public State getState() { + return state; + } + + public Object getValue() { + return value; + } + + public String getLabel() { + return String.valueOf(value); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + CheckBoxNodeData data = (CheckBoxNodeData) object; + return Objects.equals(value, data.value) && state == data.state; + } + + @Override + public int hashCode() { + return Objects.hash(value, state); + } + + @Override + public String toString() { + return "CheckBoxNodeData{" + + "value=" + value + + ", state=" + state + + '}'; + } + + public enum State { + SELECTED, + DESELECTED, + INDETERMINATE + } +} diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/swing/components/tree/CheckBoxTree.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/swing/components/tree/CheckBoxTree.java new file mode 100644 index 0000000000..1ec1ef3a9f --- /dev/null +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/swing/components/tree/CheckBoxTree.java @@ -0,0 +1,321 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.swing.components.tree; + +import javax.swing.*; +import javax.swing.event.TreeExpansionEvent; +import javax.swing.event.TreeModelEvent; +import javax.swing.event.TreeModelListener; +import javax.swing.event.TreeWillExpandListener; +import javax.swing.plaf.basic.BasicTreeUI; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +/** + * @since 5.0 + */ +public class CheckBoxTree extends JPanel { + + private static final String TOGGLE_ROW_ACTION_KEY = "toggleRow"; + private static final String SELECTION_BACKGROUND_COLOR_KEY = "Tree.selectionBackground"; + + protected final JTree labelTree; + protected final JTree checkBoxTree; + + public CheckBoxTree(TreeModel model) { + labelTree = new JTree(model); + checkBoxTree = new JTree(model); + + init(); + } + + public TreeModel getModel() { + return labelTree.getModel(); + } + + public TreeSelectionModel getSelectionModel() { + return labelTree.getSelectionModel(); + } + + private void init() { + initRender(); + initListeners(); + } + + private void initRender() { + labelTree.setRootVisible(false); + labelTree.setShowsRootHandles(true); + labelTree.setUI(new FullWidthPaintTreeUI()); + labelTree.setCellRenderer(new LabelTreeCellRenderer()); + + checkBoxTree.setRootVisible(false); + checkBoxTree.setEditable(true); + checkBoxTree.setUI(new FullWidthPaintTreeUI() { + @Override + protected int getRowX(int row, int depth) { + return 0; + } + }); + checkBoxTree.setCellRenderer(new CheckBoxTreeCellRenderer()); + checkBoxTree.setCellEditor(new CheckBoxTreeCellEditor(checkBoxTree)); + + setLayout(new BorderLayout()); + add(labelTree, BorderLayout.CENTER); + add(checkBoxTree, BorderLayout.EAST); + } + + private void initListeners() { + labelTree.getModel().addTreeModelListener(new CheckBoxTreeModelEventHandler()); + checkBoxTree.setModel(labelTree.getModel()); + + labelTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); + labelTree.addMouseListener(new TreeFullWidthMouseClickHandler(labelTree)); + checkBoxTree.setSelectionModel(labelTree.getSelectionModel()); + + labelTree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0), TOGGLE_ROW_ACTION_KEY); + checkBoxTree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0), TOGGLE_ROW_ACTION_KEY); + labelTree.getActionMap().put(TOGGLE_ROW_ACTION_KEY, new ToggleRowAction()); + checkBoxTree.getActionMap().put(TOGGLE_ROW_ACTION_KEY, new ToggleRowAction()); + + labelTree.addTreeWillExpandListener(new ShareExpandListener(checkBoxTree)); + } + + private static class CheckBoxTreeModelEventHandler implements TreeModelListener { + + private boolean listening; + + private CheckBoxTreeModelEventHandler() { + this.listening = true; + } + + @Override + public void treeNodesChanged(TreeModelEvent e) { + if (listening) { + handleEvent(e); + } + } + + @Override + public void treeNodesInserted(TreeModelEvent e) { + if (listening) { + handleEvent(e); + } + } + + @Override + public void treeNodesRemoved(TreeModelEvent e) { + if (listening) { + handleEvent(e); + } + } + + @Override + public void treeStructureChanged(TreeModelEvent e) { + if (listening) { + handleEvent(e); + } + } + + private void handleEvent(TreeModelEvent e) { + Object parentComponent = e.getTreePath().getLastPathComponent(); + if (!(e.getSource() instanceof TreeModel) || !(parentComponent instanceof DefaultMutableTreeNode)) { + return; + } + TreeModel model = (TreeModel) e.getSource(); + DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) parentComponent; + + validateParentCheckBoxState(model, e.getTreePath(), parentNode); + listening = false; + for (Object child : e.getChildren()) { + propagateCheckBoxState(model, e.getTreePath().pathByAddingChild(child), null); + } + listening = true; + } + + private void validateParentCheckBoxState(TreeModel model, TreePath parentPath, + DefaultMutableTreeNode parentNode) { + if (!(parentNode.getUserObject() instanceof CheckBoxNodeData)) { + return; + } + CheckBoxNodeData parentData = (CheckBoxNodeData) parentNode.getUserObject(); + + int childCount = model.getChildCount(parentNode); + int checkCount = countCheckedChildren(model, parentNode); + + CheckBoxNodeData parentDataChanged = + checkCount == childCount ? parentData.withState(CheckBoxNodeData.State.SELECTED) + : checkCount == 0 ? parentData.withState(CheckBoxNodeData.State.DESELECTED) + : parentData.withState(CheckBoxNodeData.State.INDETERMINATE); + model.valueForPathChanged(parentPath, parentDataChanged); + } + + private static int countCheckedChildren(TreeModel model, DefaultMutableTreeNode parentNode) { + int childCount = model.getChildCount(parentNode); + int checkCount = 0; + for (int i = 0; i < childCount; i++) { + Object childComponent = model.getChild(parentNode, i); + if (!(childComponent instanceof DefaultMutableTreeNode)) { + continue; + } + DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) childComponent; + if (!(childNode.getUserObject() instanceof CheckBoxNodeData)) { + continue; + } + CheckBoxNodeData childData = (CheckBoxNodeData) childNode.getUserObject(); + checkCount += childData.isSelected() ? 1 : 0; + } + return checkCount; + } + + private void propagateCheckBoxState(TreeModel model, TreePath childPath, CheckBoxNodeData.State parentState) { + if (parentState == CheckBoxNodeData.State.INDETERMINATE) { + return; + } + Object component = childPath.getLastPathComponent(); + if (!(component instanceof DefaultMutableTreeNode)) { + return; + } + DefaultMutableTreeNode node = (DefaultMutableTreeNode) component; + if (!(node.getUserObject() instanceof CheckBoxNodeData)) { + return; + } + CheckBoxNodeData data = (CheckBoxNodeData) node.getUserObject(); + + if (parentState != null) { + data = data.withState(parentState); + model.valueForPathChanged(childPath, data); + } + + CheckBoxNodeData finalData = data; + node.children().asIterator().forEachRemaining(child -> { + propagateCheckBoxState(model, childPath.pathByAddingChild(child), finalData.getState()); + }); + } + } + + private static class ShareExpandListener implements TreeWillExpandListener { + + private final JTree otherTree; + + private ShareExpandListener(JTree otherTree) { + this.otherTree = otherTree; + } + + @Override + public void treeWillExpand(TreeExpansionEvent event) { + otherTree.expandPath(event.getPath()); + } + + @Override + public void treeWillCollapse(TreeExpansionEvent event) { + otherTree.collapsePath(event.getPath()); + } + } + + private static class TreeFullWidthMouseClickHandler extends MouseAdapter { + + private final JTree tree; + + public TreeFullWidthMouseClickHandler(JTree tree) { + this.tree = tree; + } + + @Override + public void mousePressed(MouseEvent e) { + if (!SwingUtilities.isLeftMouseButton(e)) { + return; + } + selectFullWidth(e); + + if (e.getClickCount() < 2) { + return; + } + toggleTargetRow(e); + } + + private void selectFullWidth(MouseEvent e) { + int closestRow = tree.getClosestRowForLocation(e.getX(), e.getY()); + Rectangle closestRowBounds = tree.getRowBounds(closestRow); + if (e.getY() >= closestRowBounds.getY() + && e.getY() < closestRowBounds.getY() + closestRowBounds.getHeight() + && !tree.isRowSelected(closestRow)) { + tree.setSelectionRow(closestRow); + } + } + + private void toggleTargetRow(MouseEvent e) { + int closestRow = tree.getClosestRowForLocation(e.getX(), e.getY()); + if (tree.isExpanded(closestRow)) { + tree.collapseRow(closestRow); + } else { + tree.expandRow(closestRow); + } + } + } + + private static class FullWidthPaintTreeUI extends BasicTreeUI { + + @Override + public void paint(Graphics g, JComponent c) { + if (tree.getSelectionCount() > 0) { + g.setColor(UIManager.getColor(SELECTION_BACKGROUND_COLOR_KEY)); + int[] rows = tree.getSelectionRows(); + if (rows != null) { + for (int i : rows) { + Rectangle r = tree.getRowBounds(i); + g.fillRect(0, r.y, tree.getWidth(), r.height); + } + } + } + super.paint(g, c); + } + } + + private static class ToggleRowAction extends AbstractAction { + + @Override + public void actionPerformed(ActionEvent e) { + if (!(e.getSource() instanceof JTree)) { + return; + } + JTree tree = (JTree) e.getSource(); + if (tree.getSelectionPath() == null) { + return; + } + Object component = tree.getSelectionPath().getLastPathComponent(); + if (tree.getSelectionPath() == null || !(component instanceof DefaultMutableTreeNode)) { + return; + } + Object value = ((DefaultMutableTreeNode) component).getUserObject(); + if (!(value instanceof CheckBoxNodeData)) { + return; + } + CheckBoxNodeData data = (CheckBoxNodeData) value; + + tree.getModel().valueForPathChanged(tree.getSelectionPath(), data.toggleState()); + } + } +} diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/swing/components/tree/CheckBoxTreeCellEditor.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/swing/components/tree/CheckBoxTreeCellEditor.java new file mode 100644 index 0000000000..625d2f16ab --- /dev/null +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/swing/components/tree/CheckBoxTreeCellEditor.java @@ -0,0 +1,80 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.swing.components.tree; + +import javax.swing.*; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellEditor; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.TreeCellEditor; +import javax.swing.tree.TreePath; +import java.awt.*; +import java.awt.event.ActionEvent; + +/** + * @since 5.0 + */ +public class CheckBoxTreeCellEditor extends AbstractCellEditor implements TreeCellEditor { + + protected final CheckBoxTreeCellRenderer renderer; + protected final TreeCellEditor defaultEditor; + protected final JTree tree; + + private final AbstractAction toggleListener; + + protected CheckBoxTreeCellEditor(JTree tree) { + this.tree = tree; + this.renderer = new CheckBoxTreeCellRenderer(); + this.defaultEditor = new DefaultTreeCellEditor(tree, new DefaultTreeCellRenderer()); + + this.toggleListener = new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + Object cellEditorValue = getCellEditorValue(); + if (!(cellEditorValue instanceof CheckBoxNodeData) || !(e.getSource() instanceof JCheckBox)) { + return; + } + TreePath editingPath = tree.getEditingPath(); + JCheckBox checkBox = (JCheckBox) e.getSource(); + + CheckBoxNodeData data = (CheckBoxNodeData) cellEditorValue; + tree.getModel().valueForPathChanged(editingPath, data.withState(checkBox.isSelected())); + tree.stopEditing(); + } + }; + } + + @Override + public Object getCellEditorValue() { + DefaultMutableTreeNode editedNode = (DefaultMutableTreeNode) tree.getEditingPath().getLastPathComponent(); + return editedNode.getUserObject(); + } + + @Override + public Component getTreeCellEditorComponent(JTree tree, Object value, boolean isSelected, boolean expanded, + boolean leaf, int row) { + Component component = renderer.getTreeCellRendererComponent(tree, value, isSelected, expanded, leaf, row, true); + + if (component instanceof JCheckBox) { + JCheckBox checkBox = (JCheckBox) component; + checkBox.setAction(toggleListener); + } + return component; + } +} diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/swing/components/tree/CheckBoxTreeCellRenderer.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/swing/components/tree/CheckBoxTreeCellRenderer.java new file mode 100644 index 0000000000..8aa0642451 --- /dev/null +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/swing/components/tree/CheckBoxTreeCellRenderer.java @@ -0,0 +1,60 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.swing.components.tree; + +import javax.swing.*; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.TreeCellRenderer; +import java.awt.*; + +/** + * @since 5.0 + */ +public class CheckBoxTreeCellRenderer extends JCheckBox implements TreeCellRenderer { + + private static final String TREE_SELECTION_BACKGROUND = "Tree.selectionBackground"; + private static final String TREE_BACKGROUND = "Tree.background"; + + protected final TreeCellRenderer defaultRenderer; + + public CheckBoxTreeCellRenderer() { + defaultRenderer = new DefaultTreeCellRenderer(); + setOpaque(false); + } + + @Override + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, + boolean leaf, int row, boolean hasFocus) { + if (!(value instanceof DefaultMutableTreeNode)) { + return defaultRenderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); + } + DefaultMutableTreeNode node = (DefaultMutableTreeNode) value; + Object userObject = node.getUserObject(); + if (!(userObject instanceof CheckBoxNodeData)) { + return defaultRenderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); + } + CheckBoxNodeData data = ((CheckBoxNodeData) node.getUserObject()); + + setBackground(UIManager.getColor(selected ? TREE_SELECTION_BACKGROUND : TREE_BACKGROUND)); + setSelected(data.getState() != CheckBoxNodeData.State.INDETERMINATE && data.isSelected()); + + return this; + } +} diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/swing/components/tree/LabelTreeCellRenderer.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/swing/components/tree/LabelTreeCellRenderer.java new file mode 100644 index 0000000000..b5fe0d09b3 --- /dev/null +++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/swing/components/tree/LabelTreeCellRenderer.java @@ -0,0 +1,54 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.swing.components.tree; + +import javax.swing.*; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.TreeCellRenderer; +import java.awt.*; + +/** + * @since 5.0 + */ +public class LabelTreeCellRenderer extends JLabel implements TreeCellRenderer { + + protected final TreeCellRenderer defaultRenderer; + + LabelTreeCellRenderer() { + defaultRenderer = new DefaultTreeCellRenderer(); + setOpaque(false); + } + + @Override + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, + boolean leaf, int row, boolean hasFocus) { + if (!(value instanceof DefaultMutableTreeNode)) { + return defaultRenderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); + } + DefaultMutableTreeNode node = (DefaultMutableTreeNode) value; + if (!(node.getUserObject() instanceof CheckBoxNodeData)) { + return defaultRenderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); + } + CheckBoxNodeData data = ((CheckBoxNodeData) node.getUserObject()); + + setText(data.getLabel()); + return this; + } +} diff --git a/modeler/cayenne-modeler/src/main/resources/org/apache/cayenne/modeler/images/icon-disable.png b/modeler/cayenne-modeler/src/main/resources/org/apache/cayenne/modeler/images/icon-disable.png new file mode 100644 index 0000000000..522ba12319 Binary files /dev/null and b/modeler/cayenne-modeler/src/main/resources/org/apache/cayenne/modeler/images/icon-disable.png differ diff --git a/modeler/cayenne-modeler/src/test/java/org/apache/cayenne/modeler/CayenneModelerValidationIT.java b/modeler/cayenne-modeler/src/test/java/org/apache/cayenne/modeler/CayenneModelerValidationIT.java new file mode 100644 index 0000000000..9c298be3a2 --- /dev/null +++ b/modeler/cayenne-modeler/src/test/java/org/apache/cayenne/modeler/CayenneModelerValidationIT.java @@ -0,0 +1,86 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.modeler; + +import org.apache.cayenne.configuration.DataChannelDescriptor; +import org.apache.cayenne.dbsync.reverse.configuration.ToolsModule; +import org.apache.cayenne.di.DIBootstrap; +import org.apache.cayenne.di.Injector; +import org.apache.cayenne.modeler.init.CayenneModelerModule; +import org.apache.cayenne.modeler.validation.ConfigurableProjectValidator; +import org.apache.cayenne.project.Project; +import org.apache.cayenne.project.ProjectLoader; +import org.apache.cayenne.project.ProjectModule; +import org.apache.cayenne.project.validation.Inspection; +import org.apache.cayenne.project.validation.ProjectValidator; +import org.apache.cayenne.project.validation.ValidationConfig; +import org.apache.cayenne.resource.URLResource; +import org.junit.BeforeClass; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.EnumSet; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class CayenneModelerValidationIT { + + public static final String CAYENNE_CONFIGURED_VALIDATION_PROJECT = "cayenne-configured-validation.xml"; + + private final static Logger logger = LoggerFactory.getLogger(CayenneModelerValidationIT.class); + private static Injector injector; + + @BeforeClass + public static void setUp() { + injector = DIBootstrap.createInjector(List.of( + new ToolsModule(logger), + new ProjectModule(), + new CayenneModelerModule() + )); + } + + @Test + public void validatorProvided() { + ProjectValidator projectValidator = injector.getInstance(ProjectValidator.class); + assertTrue(projectValidator instanceof ConfigurableProjectValidator); + } + + @Test + public void configLoaded() { + URLResource projectResource = new URLResource(getClass().getResource(CAYENNE_CONFIGURED_VALIDATION_PROJECT)); + Application application = injector.getInstance(Application.class); + ProjectLoader projectLoader = injector.getInstance(ProjectLoader.class); + Project project = projectLoader.loadProject(projectResource); + + DataChannelDescriptor dataChannel = (DataChannelDescriptor) project.getRootNode(); + ValidationConfig config = ValidationConfig.fromMetadata(application.getMetaData(), dataChannel); + + assertNotNull(config); + assertEquals(EnumSet.complementOf(EnumSet.of( + Inspection.DATA_CHANNEL_NO_NAME, + Inspection.DATA_NODE_NO_NAME, + Inspection.DATA_NODE_NAME_DUPLICATE, + Inspection.DATA_NODE_CONNECTION_PARAMS + )), config.getEnabledInspections()); + } +} diff --git a/modeler/cayenne-modeler/src/test/resources/org/apache/cayenne/modeler/cayenne-configured-validation.xml b/modeler/cayenne-modeler/src/test/resources/org/apache/cayenne/modeler/cayenne-configured-validation.xml new file mode 100644 index 0000000000..8deb69daa9 --- /dev/null +++ b/modeler/cayenne-modeler/src/test/resources/org/apache/cayenne/modeler/cayenne-configured-validation.xml @@ -0,0 +1,12 @@ + + + + DATA_CHANNEL_NO_NAME + DATA_NODE_NO_NAME + DATA_NODE_NAME_DUPLICATE + DATA_NODE_CONNECTION_PARAMS + +