diff --git a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/base/builder/AbstractBuilder.java b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/base/builder/AbstractBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..8453085160f72847fee1f6f387335e653433543b --- /dev/null +++ b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/base/builder/AbstractBuilder.java @@ -0,0 +1,45 @@ +package de.unikoblenz.fgbks.base.builder; + +import java.io.Serializable; +import java.lang.reflect.Field; + +public abstract class AbstractBuilder<E, B extends AbstractBuilder<E, B>> implements Serializable { + private static final String VALUE_FIELD_NAME = "value"; + + protected E value; + + protected AbstractBuilder() { + value = newInstance(); + } + + protected AbstractBuilder(E v) { + value = v; + } + + public static Field getAccessibleValueField() throws IllegalStateException { + try { + Field valueField = AbstractBuilder.class.getDeclaredField(VALUE_FIELD_NAME); + valueField.setAccessible(true); + return valueField; + } catch (NoSuchFieldException e) { + throw new IllegalStateException( + String.format( + "%s must have field with name '%s'", + AbstractBuilder.class.getSimpleName(), VALUE_FIELD_NAME), + e); + } + } + + protected abstract B thisBuilder(); + + protected abstract E newInstance(); + + protected abstract void validate(); + + public E build() { + validate(); + E result = value; + value = newInstance(); + return result; + } +} diff --git a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/base/builder/DefaultBuilder.java b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/base/builder/DefaultBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..c57b946c7b251f6c8c823d6e6e2a869a6374075b --- /dev/null +++ b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/base/builder/DefaultBuilder.java @@ -0,0 +1,79 @@ +package de.unikoblenz.fgbks.base.builder; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +/** @param <E> The type of the object being constructed. */ +public class DefaultBuilder<E> extends AbstractBuilder<E, DefaultBuilder<E>> { + + protected DefaultBuilder() { + super(); + } + + protected DefaultBuilder(E value) { + super(value); + } + + /** + * Liefert E so zurück + * + * @return DefaultBuilder + */ + @Override + protected DefaultBuilder<E> thisBuilder() { + return this; + } + + @Override + protected void validate() { + // nothing to validate + } + + /** + * @return <tt>new E()</tt>, where E is the type of the object being constructed which is the type + * parameter of this class. + * @throws IllegalStateException + * <ul> + * <li>when the type parameter cannot be extracted or is no {@link Class}, but a generic + * type + * <li>or when no default-constructor can be found + * <li>or when the constructor throws an exception other than {@link RuntimeException} + * </ul> + */ + @Override + protected E newInstance() { + Class<?> directSubclass = getClass(); + while (directSubclass.getSuperclass() != DefaultBuilder.class) { + directSubclass = directSubclass.getSuperclass(); + } + Type genericSuperclass = directSubclass.getGenericSuperclass(); + if (!(genericSuperclass instanceof ParameterizedType)) { + throw new IllegalStateException( + "Generic type argument E missing for superclass " + DefaultBuilder.class.getSimpleName()); + } + ParameterizedType parameterizedSuperclass = (ParameterizedType) genericSuperclass; + Type valueType = parameterizedSuperclass.getActualTypeArguments()[0]; + if (!(valueType instanceof Class)) { + throw new IllegalStateException( + "No generics allowed for type argument, but found " + valueType); + } + Class<E> type = (Class<E>) valueType; + try { + Constructor<E> constructor = type.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } catch (NoSuchMethodException e) { + throw new IllegalStateException("No default-constructor found for " + type.getName(), e); + } catch (InvocationTargetException e) { + if (e.getTargetException() instanceof RuntimeException) { + throw (RuntimeException) e.getTargetException(); + } else { + throw new IllegalStateException(e.getTargetException()); + } + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/api/APIExceptionInterceptor.java b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/base/utils/ExceptionInterceptor.java similarity index 84% rename from dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/api/APIExceptionInterceptor.java rename to dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/base/utils/ExceptionInterceptor.java index 05d07af4f351d383ba6e6699485a89cf2a4cc0fc..728db5531c4998f77376b237606779b158489620 100644 --- a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/api/APIExceptionInterceptor.java +++ b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/base/utils/ExceptionInterceptor.java @@ -1,10 +1,10 @@ -package de.unikoblenz.fgbks.dmn.api; +package de.unikoblenz.fgbks.base.utils; import javax.interceptor.AroundInvoke; import javax.interceptor.InvocationContext; import javax.ws.rs.core.Response; -public class APIExceptionInterceptor { +public class ExceptionInterceptor { @AroundInvoke public Object value(InvocationContext context) { diff --git a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/base/utils/Performance.java b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/base/utils/Performance.java new file mode 100644 index 0000000000000000000000000000000000000000..323ef5c0ac68ebd5fc3fe8145bbc13443b282794 --- /dev/null +++ b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/base/utils/Performance.java @@ -0,0 +1,14 @@ +package de.unikoblenz.fgbks.base.utils; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import javax.interceptor.InterceptorBinding; + +@InterceptorBinding +@Target({METHOD, TYPE}) +@Retention(RUNTIME) +public @interface Performance {} diff --git a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/base/utils/PerformanceInterceptor.java b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/base/utils/PerformanceInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..c47d27d9d7677155c85ba4cddfc83e2850df263d --- /dev/null +++ b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/base/utils/PerformanceInterceptor.java @@ -0,0 +1,26 @@ +package de.unikoblenz.fgbks.base.utils; + +import java.io.Serializable; +import java.util.concurrent.TimeUnit; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InvocationContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Performance +@Interceptor +public class PerformanceInterceptor implements Serializable { + + @AroundInvoke + public Object performance(InvocationContext ctx) throws Exception { + long start = System.nanoTime(); + Object obj = ctx.proceed(); + long end = System.nanoTime(); + long durationInMs = TimeUnit.NANOSECONDS.toMillis(end - start); + + Logger log = LoggerFactory.getLogger(ctx.getMethod().getDeclaringClass()); + log.info(ctx.getMethod().getName() + " - " + durationInMs + " ms"); + return obj; + } +} diff --git a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/wordnet/WordnetService.java b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/base/wordnet/WordnetService.java similarity index 93% rename from dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/wordnet/WordnetService.java rename to dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/base/wordnet/WordnetService.java index bf327efba26ce01b84abf742bc66526ad8eaf4e4..915ce7c8816b5df8026d26b5e092972b960774a9 100644 --- a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/wordnet/WordnetService.java +++ b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/base/wordnet/WordnetService.java @@ -1,4 +1,4 @@ -package de.unikoblenz.fgbks.dmn.core.verifier.helper.wordnet; +package de.unikoblenz.fgbks.base.wordnet; import edu.mit.jwi.Dictionary; import edu.mit.jwi.IDictionary; @@ -85,4 +85,10 @@ public class WordnetService { } return false; } + + public void close() { + if (dictionary != null && dictionary.isOpen()) { + dictionary.close(); + } + } } diff --git a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/api/DmnApi.java b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/api/DmnApi.java index e3aaac7cbaefd7bf6a8eb836d17451d80d7fe50c..fda7274d70ff07c3fa2899f9164f81fdaaefea3a 100644 --- a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/api/DmnApi.java +++ b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/api/DmnApi.java @@ -1,5 +1,7 @@ package de.unikoblenz.fgbks.dmn.api; +import de.unikoblenz.fgbks.base.utils.ExceptionInterceptor; +import de.unikoblenz.fgbks.base.utils.PerformanceInterceptor; import de.unikoblenz.fgbks.dmn.beans.DmnBean; import de.unikoblenz.fgbks.dmn.core.DmnService; import de.unikoblenz.fgbks.dmn.core.models.VerifierType; @@ -17,7 +19,7 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; @Path("/dmn") -@Interceptors(APIExceptionInterceptor.class) +@Interceptors({ExceptionInterceptor.class, PerformanceInterceptor.class}) public class DmnApi { private static final String NO_CURRENT_DMN_FOUND = "No current DMN found"; @@ -41,17 +43,6 @@ public class DmnApi { return Response.status(Status.OK).entity(dmnBean.getDmnXML()).build(); } - @GET - @Produces({MediaType.APPLICATION_JSON}) - @Path("/test") - public Response getTestDmnErrors() { - Optional<DmnService> dmnHandler = createDmnService(); - if (dmnHandler.isPresent()) { - return Response.status(Status.OK).entity(dmnHandler.get().getTestValidationErrors()).build(); - } - return Response.serverError().entity(NO_CURRENT_DMN_FOUND).build(); - } - @GET @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) @Consumes({MediaType.TEXT_PLAIN}) diff --git a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/beans/StartupListener.java b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/beans/StartupListener.java new file mode 100644 index 0000000000000000000000000000000000000000..aa53277f195014485742b06f9b9f98fda9221d4b --- /dev/null +++ b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/beans/StartupListener.java @@ -0,0 +1,28 @@ +package de.unikoblenz.fgbks.dmn.beans; + +import de.unikoblenz.fgbks.base.wordnet.WordnetService; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.Destroyed; +import javax.enterprise.context.Initialized; +import javax.enterprise.event.Observes; +import javax.servlet.ServletContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ApplicationScoped +public class StartupListener { + + private static Logger LOGGER = LoggerFactory.getLogger(StartupListener.class.getSimpleName()); + + public void init(@Observes @Initialized(ApplicationScoped.class) ServletContext context) { + // init wordnet + LOGGER.info("init Wordnet"); + WordnetService.getInstance(); + } + + public void destroy(@Observes @Destroyed(ApplicationScoped.class) ServletContext context) { + // close wordnet + LOGGER.info("close wordnet"); + WordnetService.getInstance().close(); + } +} diff --git a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/DmnService.java b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/DmnService.java index 71d4c9dfce16083e7a269025ac4931d082ac6350..cfca8a3e1b0311487335761090cdb6eb4c6b8fc5 100644 --- a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/DmnService.java +++ b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/DmnService.java @@ -1,10 +1,7 @@ package de.unikoblenz.fgbks.dmn.core; -import de.unikoblenz.fgbks.dmn.core.models.RuleIdentifier; -import de.unikoblenz.fgbks.dmn.core.models.VerificationResult; import de.unikoblenz.fgbks.dmn.core.models.VerifierCollectionResult; import de.unikoblenz.fgbks.dmn.core.models.VerifierResult; -import de.unikoblenz.fgbks.dmn.core.models.VerifierResult.Builder; import de.unikoblenz.fgbks.dmn.core.models.VerifierType; import java.io.InputStream; import java.util.ArrayList; @@ -13,8 +10,6 @@ import java.util.Objects; import org.apache.commons.io.IOUtils; import org.camunda.bpm.dmn.engine.DmnDecision; import org.camunda.bpm.dmn.engine.DmnEngineConfiguration; -import org.camunda.bpm.dmn.engine.impl.DmnDecisionTableImpl; -import org.camunda.bpm.dmn.engine.impl.DmnDecisionTableRuleImpl; public class DmnService { @@ -47,31 +42,6 @@ public class DmnService { return sb.toString(); } - public VerifierResult getTestValidationErrors() { - Builder resultBuilder = - VerifierResult.getBuilder().withVerifierType(VerifierType.IdenticalRules); - - for (DmnDecision d : dmnDecisions) { - if (d.isDecisionTable()) { - DmnDecisionTableImpl table = (DmnDecisionTableImpl) d.getDecisionLogic(); - int counter = 0; - VerificationResult.Builder vBuilder = - VerificationResult.getBuilder().withMessage(d.getName() + " Verification!!!"); - for (DmnDecisionTableRuleImpl rule : table.getRules()) { - vBuilder.addRule( - RuleIdentifier.getBuilder() - .withDecisionKey(d.getKey()) - .withDecisionName(d.getName()) - .withRuleId(rule.getId()) - .withRowNumber(++counter) - .build()); - } - resultBuilder.addVerificationResult(vBuilder.build()); - } - } - return resultBuilder.build(); - } - public VerifierResult getVerifierFromType(VerifierType verifierType) { return verifierType.getResult(dmnDecisions, null).orElse(null); } diff --git a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/models/RuleIdentifier.java b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/models/RuleIdentifier.java index 828bb5924e30e09653e96165188bf613f887f692..92ae294f4ec732653ca2501b29c1a0a99ea36ae3 100644 --- a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/models/RuleIdentifier.java +++ b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/models/RuleIdentifier.java @@ -1,5 +1,6 @@ package de.unikoblenz.fgbks.dmn.core.models; +import de.unikoblenz.fgbks.base.builder.DefaultBuilder; import java.io.Serializable; import java.util.Objects; import javax.xml.bind.annotation.XmlElement; @@ -95,56 +96,52 @@ public class RuleIdentifier implements Serializable { return Objects.hash(decisionKey, ruleId, conditionId); } - public class Builder { + public class Builder extends DefaultBuilder<RuleIdentifier> { private Builder() {} public Builder withDecision(DmnDecision d) { - setDecisionKey(d.getKey()); - setDecisionName(d.getName()); + value.setDecisionKey(d.getKey()); + value.setDecisionName(d.getName()); return this; } public Builder withDecisionKey(String decisionKey) { - setDecisionKey(decisionKey); + value.setDecisionKey(decisionKey); return this; } public Builder withDecisionName(String decisionName) { - setDecisionName(decisionName); + value.setDecisionName(decisionName); return this; } public Builder withRuleId(String ruleId) { - setRuleId(ruleId); + value.setRuleId(ruleId); return this; } public Builder withRowNumber(Integer rowNumber) { - setRowNumber(rowNumber); + value.setRowNumber(rowNumber); return this; } public Builder withConditionId(String conditionId) { - setConditionId(conditionId); + value.setConditionId(conditionId); return this; } public Builder withConditionName(String conditionName) { - setConditionName(conditionName); + value.setConditionName(conditionName); return this; } - private RuleIdentifier validate() { - Objects.requireNonNull(decisionKey); - Objects.requireNonNull(decisionName); - Objects.requireNonNull(ruleId); - Objects.requireNonNull(rowNumber); - return RuleIdentifier.this; - } - - public RuleIdentifier build() { - return validate(); + @Override + protected void validate() { + Objects.requireNonNull(value.decisionKey); + Objects.requireNonNull(value.decisionName); + Objects.requireNonNull(value.ruleId); + Objects.requireNonNull(value.rowNumber); } } } diff --git a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/models/VerificationResult.java b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/models/VerificationResult.java index 4494dade082bce842cb990ebde468c566c5cac29..f1784f3af37d5eec3b0522fd9f79ff8a1311e5be 100644 --- a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/models/VerificationResult.java +++ b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/models/VerificationResult.java @@ -1,5 +1,6 @@ package de.unikoblenz.fgbks.dmn.core.models; +import de.unikoblenz.fgbks.base.builder.DefaultBuilder; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; @@ -43,37 +44,31 @@ public class VerificationResult implements Serializable { return new VerificationResult().new Builder(); } - public class Builder { - - private Builder() {} + public class Builder extends DefaultBuilder<VerificationResult> { public Builder addRule(RuleIdentifier ruleIdentifier) { - rules.add(ruleIdentifier); + value.rules.add(ruleIdentifier); return this; } public Builder addRules(Collection<RuleIdentifier> ruleIdentifier) { - rules.addAll(ruleIdentifier); + value.rules.addAll(ruleIdentifier); return this; } public Builder withMessage(String message, Object... args) { - setMessage(String.format(message, args)); + value.setMessage(String.format(message, args)); return this; } public Builder withMessage(String message) { - setMessage(message); + value.setMessage(message); return this; } - private VerificationResult validate() { - Objects.requireNonNull(message); - return VerificationResult.this; - } - - public VerificationResult build() { - return validate(); + @Override + protected void validate() { + Objects.requireNonNull(value.message); } } } diff --git a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/models/VerifierCollectionResult.java b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/models/VerifierCollectionResult.java index f91403de454b465c02cf2a69965468685f50df44..b6a7c0e732cfaec9786d8a5396e2a3dd756ec05a 100644 --- a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/models/VerifierCollectionResult.java +++ b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/models/VerifierCollectionResult.java @@ -1,5 +1,6 @@ package de.unikoblenz.fgbks.dmn.core.models; +import de.unikoblenz.fgbks.base.builder.DefaultBuilder; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -24,21 +25,11 @@ public class VerifierCollectionResult { return new VerifierCollectionResult().new Builder(); } - public class Builder { - - private Builder() {} + public class Builder extends DefaultBuilder<VerifierCollectionResult> { public VerifierCollectionResult.Builder addVerification(VerifierResult verificationResult) { - verifierResults.add(Objects.requireNonNull(verificationResult)); + value.verifierResults.add(Objects.requireNonNull(verificationResult)); return this; } - - private VerifierCollectionResult validate() { - return VerifierCollectionResult.this; - } - - public VerifierCollectionResult build() { - return validate(); - } } } diff --git a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/models/VerifierResult.java b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/models/VerifierResult.java index de4e8db1d84177682ee1ecdef85b35da7c2c6485..1258472f8f3f2b06f3e8f38cc947f00a248005b4 100644 --- a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/models/VerifierResult.java +++ b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/models/VerifierResult.java @@ -1,5 +1,6 @@ package de.unikoblenz.fgbks.dmn.core.models; +import de.unikoblenz.fgbks.base.builder.DefaultBuilder; import java.io.Serializable; import java.util.HashSet; import java.util.Objects; @@ -48,27 +49,21 @@ public class VerifierResult implements Serializable { return new VerifierResult().new Builder(); } - public class Builder { - - private Builder() {} + public class Builder extends DefaultBuilder<VerifierResult> { public Builder withVerifierType(VerifierType verifierType) { - setVerifierType(verifierType); + value.setVerifierType(verifierType); return this; } public Builder addVerificationResult(VerificationResult ruleIdentifier) { - verifications.add(ruleIdentifier); + value.verifications.add(ruleIdentifier); return this; } - private VerifierResult validate() { - Objects.requireNonNull(verifierType); - return VerifierResult.this; - } - - public VerifierResult build() { - return validate(); + @Override + protected void validate() { + Objects.requireNonNull(value.verifierType); } } } diff --git a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/models/VerifierType.java b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/models/VerifierType.java index 1d7851120ab280f16f3e1c669a8f47d1fc1ffe87..0bb3657802f7d2e10b0577bb150ed543dff8ab47 100644 --- a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/models/VerifierType.java +++ b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/models/VerifierType.java @@ -1,24 +1,23 @@ package de.unikoblenz.fgbks.dmn.core.models; -import de.unikoblenz.fgbks.dmn.core.verifier.AbstractVerifier; -import de.unikoblenz.fgbks.dmn.core.verifier.EquivalentRules; -import de.unikoblenz.fgbks.dmn.core.verifier.IdenticalRules; -import de.unikoblenz.fgbks.dmn.core.verifier.SubsumptionRules; +import de.unikoblenz.fgbks.dmn.core.verifier.*; import de.unikoblenz.fgbks.dmn.core.verifier.helper.RuleMap; import java.util.List; import java.util.Optional; import org.camunda.bpm.dmn.engine.DmnDecision; public enum VerifierType { - IdenticalRules("Identical Business Rule", IdenticalRules.class), + Identical("Identical Business Rule", IdenticalRules.class), Subsumption("Business Rules, that are subsumption", SubsumptionRules.class), - Equivalent("Checking for synonyms in columns.", EquivalentRules.class); + Equivalent("Checking for synonyms in columns.", EquivalentRules.class), + Overlap("Checking for overlapping rules.", OverlappingRules.class), + Missing("Checking for missing rules", MissingRules.class); /* , Overlap("", null), Contradictions("", null), SpecificPartialReduction("", null), - MissingRules("", null), + Missing("", null), */ private final Class<? extends AbstractVerifier> verifierClass; private String description; @@ -34,7 +33,7 @@ public enum VerifierType { public Optional<VerifierResult> getResult(List<DmnDecision> dmnDecisionList, RuleMap ruleMap) { if (ruleMap == null) { - ruleMap = RuleMap.createMapFromDMN(dmnDecisionList); + ruleMap = RuleMap.createMapFromDmn(dmnDecisionList); } try { return Optional.of( @@ -45,7 +44,7 @@ public enum VerifierType { } public static VerifierCollectionResult getAllResults(List<DmnDecision> dmnDecisionList) { - RuleMap ruleMap = RuleMap.createMapFromDMN(dmnDecisionList); + RuleMap ruleMap = RuleMap.createMapFromDmn(dmnDecisionList); VerifierCollectionResult.Builder builder = VerifierCollectionResult.getBuilder(); for (VerifierType verifierType : values()) { // TODO: make multi-thread diff --git a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/EquivalentRules.java b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/EquivalentRules.java index b7b6bab2fcf51582d524bdc006b47479a251fd3e..93c049d2fe012b0800acbe30164beb89b1b83b8c 100644 --- a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/EquivalentRules.java +++ b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/EquivalentRules.java @@ -1,11 +1,11 @@ package de.unikoblenz.fgbks.dmn.core.verifier; +import de.unikoblenz.fgbks.base.wordnet.WordnetService; import de.unikoblenz.fgbks.dmn.core.models.VerificationResult; import de.unikoblenz.fgbks.dmn.core.models.VerifierType; import de.unikoblenz.fgbks.dmn.core.verifier.helper.DataType; import de.unikoblenz.fgbks.dmn.core.verifier.helper.Type; import de.unikoblenz.fgbks.dmn.core.verifier.helper.Value; -import de.unikoblenz.fgbks.dmn.core.verifier.helper.wordnet.WordnetService; import java.util.List; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -50,7 +50,7 @@ public class EquivalentRules extends AbstractVerifier { VerificationResult.getBuilder() .addRule(values.get(i).getRuleIdentifier()) .addRule(values.get(u).getRuleIdentifier()) - .withMessage("%s and %s: equal meaning? are they synonyms.", val1, val2) + .withMessage("%s and %s: equal meaning? Are they synonyms?", val1, val2) .build()); } } diff --git a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/IdenticalRules.java b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/IdenticalRules.java index ff3ebed67bfb317c1eff8f628f554c727d295613..45ec8a484f71a0d4fc2cb0c0019f7045d47de33a 100644 --- a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/IdenticalRules.java +++ b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/IdenticalRules.java @@ -8,6 +8,7 @@ import de.unikoblenz.fgbks.dmn.core.verifier.helper.Value; import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Set; import java.util.logging.Logger; import java.util.stream.Collectors; import org.camunda.bpm.dmn.engine.DmnDecision; @@ -15,23 +16,26 @@ import org.camunda.bpm.dmn.engine.DmnDecision; public class IdenticalRules extends AbstractVerifier { private static final Logger LOGGER = Logger.getLogger(IdenticalRules.class.getSimpleName()); + private Set<Set<String>> identicaldecicionTables; public IdenticalRules() { - super(VerifierType.IdenticalRules); + super(VerifierType.Identical); } @Override - protected void beforeVerifyDecision() {} + protected void beforeVerifyDecision() { + identicaldecicionTables = ruleMap.getIncludingInputDefinitions(); + } @Override protected void verifyDecision(DmnDecision d) { if (d.isDecisionTable()) { List<Type> inputs = new ArrayList<>(ruleMap.getAllInputTypesFromDecisionKey(d.getKey())); - checkForEqualRules(inputs, 0, null); + checkForIdenticalRules(inputs, 0, null); } } - private void checkForEqualRules( + private void checkForIdenticalRules( List<Type> inputs, int i, List<RuleIdentifier> currentRuleIdentifiers) { if (i == inputs.size()) { VerificationResult.Builder vBuilder = @@ -51,7 +55,7 @@ public class IdenticalRules extends AbstractVerifier { } else { if (currentBounds.size() > 0) { currentBounds.add(lastBound); - checkForEqualRules( + checkForIdenticalRules( inputs, i + 1, currentBounds @@ -66,7 +70,7 @@ public class IdenticalRules extends AbstractVerifier { } if (currentBounds.size() > 0) { currentBounds.add(lastBound); - checkForEqualRules( + checkForIdenticalRules( inputs, i + 1, currentBounds.stream().map(Value::getRuleIdentifier).collect(Collectors.toList())); diff --git a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/MissingRules.java b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/MissingRules.java new file mode 100644 index 0000000000000000000000000000000000000000..c587154318141f50b7bafdfc8e3a0fca184bb8e7 --- /dev/null +++ b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/MissingRules.java @@ -0,0 +1,87 @@ +package de.unikoblenz.fgbks.dmn.core.verifier; + +import de.unikoblenz.fgbks.dmn.core.models.RuleIdentifier; +import de.unikoblenz.fgbks.dmn.core.models.VerificationResult; +import de.unikoblenz.fgbks.dmn.core.models.VerifierType; +import de.unikoblenz.fgbks.dmn.core.verifier.helper.Boundary; +import de.unikoblenz.fgbks.dmn.core.verifier.helper.Type; +import de.unikoblenz.fgbks.dmn.core.verifier.helper.Value; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import org.camunda.bpm.dmn.engine.DmnDecision; + +public class MissingRules extends AbstractVerifier { + + public MissingRules() { + super(VerifierType.Missing); + } + + @Override + protected void beforeVerifyDecision() {} + + @Override + protected void verifyDecision(DmnDecision d) { + if (d.isDecisionTable()) { + List<Type> inputs = new ArrayList<>(ruleMap.getAllInputTypesFromDecisionKey(d.getKey())); + checkForMissingRules(d, inputs, 0, null, new ArrayList<>()); + } + } + + private void checkForMissingRules( + DmnDecision d, + List<Type> inputs, + int i, + List<RuleIdentifier> currentRuleIdentifiers, + List<Boundary> missingIntervals) { + + List<Value> sortedBounds = + new ArrayList<>(ruleMap.getValuesFromInputType(inputs.get(i), currentRuleIdentifiers)); + sortedBounds.sort(Comparator.comparing(Value::getBoundary)); + + Value lastBound = null; + for (Value currentBound : sortedBounds) { + if (lastBound == null) { + if (currentBound.getBoundary().getLowerBound() != Double.MIN_VALUE) { + missingIntervals.add(currentBound.getBoundary().getNewBoundaryLower().get()); + } + } else { + boolean contiguous = + lastBound.getBoundary().combineWith(currentBound.getBoundary()).isPresent(); + if (!contiguous) { + Boundary newBound = + lastBound.getBoundary().getNewBoundaryBetween(currentBound.getBoundary()).get(); + missingIntervals.add(newBound); + } + } + if (lastBound == null + || !lastBound + .getBoundary() + .isNumberInRange( + currentBound.getBoundary().getUpperBound(), + currentBound.getBoundary().getUpperBoundType())) { + lastBound = currentBound; + } + } + if (lastBound != null) { + if (lastBound.getBoundary().getUpperBound() != Double.MAX_VALUE) { + missingIntervals.add(lastBound.getBoundary().getNewBoundaryUpper().get()); + } + } + for (Boundary b : missingIntervals) { + addVerification( + VerificationResult.getBuilder() + .addRule( + RuleIdentifier.getBuilder() + .withRowNumber(999) + .withDecision(d) + .withRuleId("a") + .build()) + .withMessage("Missing interval: %s", b.toString()) + .build()); + } + } + + @Override + protected void afterVerifyDecision() {} +} diff --git a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/OverlappingRules.java b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/OverlappingRules.java new file mode 100644 index 0000000000000000000000000000000000000000..6ebc9c1f8b4ed0ac7695ea5cce6a1eb370178806 --- /dev/null +++ b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/OverlappingRules.java @@ -0,0 +1,94 @@ +package de.unikoblenz.fgbks.dmn.core.verifier; + +import de.unikoblenz.fgbks.dmn.core.models.RuleIdentifier; +import de.unikoblenz.fgbks.dmn.core.models.VerificationResult; +import de.unikoblenz.fgbks.dmn.core.models.VerifierType; +import de.unikoblenz.fgbks.dmn.core.verifier.helper.Type; +import de.unikoblenz.fgbks.dmn.core.verifier.helper.Value; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; +import org.camunda.bpm.dmn.engine.DmnDecision; + +public class OverlappingRules extends AbstractVerifier { + + public OverlappingRules() { + super(VerifierType.Overlap); + } + + @Override + protected void beforeVerifyDecision() {} + + @Override + protected void verifyDecision(DmnDecision d) { + if (d.isDecisionTable()) { + List<Type> inputs = new ArrayList<>(ruleMap.getAllInputTypesFromDecisionKey(d.getKey())); + checkForOverlappingRules(inputs, 0, null, false); + } + } + + private void checkForOverlappingRules( + List<Type> inputs, int i, List<RuleIdentifier> currentRuleIdentifiers, boolean hasOverlap) { + if (i == inputs.size()) { + if (hasOverlap) { + VerificationResult.Builder vBuilder = + VerificationResult.getBuilder().withMessage(getMessageText(currentRuleIdentifiers)); + vBuilder.addRules(currentRuleIdentifiers); + addVerification(vBuilder.build()); + } + } else { + List<Value> currentBounds = new ArrayList<>(); + List<Value> sortedBounds = + new ArrayList<>(ruleMap.getValuesFromInputType(inputs.get(i), currentRuleIdentifiers)); + sortedBounds.sort(Comparator.comparing(Value::getBoundary)); + Value lastBound = null; + boolean foundOverlap = false; + for (Value currentBound : sortedBounds) { + if (lastBound != null) { + boolean isOverlap = + currentBound.getBoundary().isBoundaryOverlapping(lastBound.getBoundary()); + if (isOverlap || currentBound.getBoundary().isBoundaryEquals(lastBound.getBoundary())) { + currentBounds.add(lastBound); + if (isOverlap) { + foundOverlap = true; + } + } else { + if (currentBounds.size() > 0) { + currentBounds.add(lastBound); + checkForOverlappingRules( + inputs, + i + 1, + currentBounds.stream().map(Value::getRuleIdentifier).collect(Collectors.toList()), + foundOverlap || hasOverlap); + } + currentBounds.clear(); + } + } + lastBound = currentBound; + } + if (currentBounds.size() > 0) { + currentBounds.add(lastBound); + checkForOverlappingRules( + inputs, + i + 1, + currentBounds.stream().map(Value::getRuleIdentifier).collect(Collectors.toList()), + foundOverlap || hasOverlap); + } + } + } + + @Override + protected void afterVerifyDecision() {} + + private String getMessageText(List<RuleIdentifier> currentRuleIdentifiers) { + StringBuilder sb = new StringBuilder("Rules "); + sb.append( + currentRuleIdentifiers + .stream() + .map(c -> c.getRowNumber().toString()) + .collect(Collectors.joining(", "))); + sb.append(" are overlapping."); + return sb.toString(); + } +} diff --git a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/SubsumptionRules.java b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/SubsumptionRules.java index 2e982a2e28c61a389b23bf90aa2b55b7db0cc974..1304fea522434927b50b6d3fb62a42663f7720d8 100644 --- a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/SubsumptionRules.java +++ b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/SubsumptionRules.java @@ -6,8 +6,10 @@ import de.unikoblenz.fgbks.dmn.core.models.VerifierType; import de.unikoblenz.fgbks.dmn.core.verifier.helper.Type; import de.unikoblenz.fgbks.dmn.core.verifier.helper.Value; import java.util.ArrayList; -import java.util.Comparator; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import org.camunda.bpm.dmn.engine.DmnDecision; @@ -18,13 +20,14 @@ public class SubsumptionRules extends AbstractVerifier { } @Override - protected void beforeVerifyDecision() {} + protected void beforeVerifyDecision() { + } @Override protected void verifyDecision(DmnDecision d) { if (d.isDecisionTable()) { List<Type> inputs = new ArrayList<>(ruleMap.getAllInputTypesFromDecisionKey(d.getKey())); - checkForSubsumptionRules(inputs, 0, null, false); + checkForSubsumptionRules(inputs, 0, null, false, null); } } @@ -32,7 +35,7 @@ public class SubsumptionRules extends AbstractVerifier { List<Type> inputs, int i, List<RuleIdentifier> currentRuleIdentifiers, - boolean hasSubsumption) { + boolean hasSubsumption, Value currentRootSubsumptionElement) { if (i == inputs.size()) { if (hasSubsumption) { VerificationResult.Builder vBuilder = @@ -41,49 +44,79 @@ public class SubsumptionRules extends AbstractVerifier { addVerification(vBuilder.build()); } } else { - List<Value> currentBounds = new ArrayList<>(); - List<Value> sortedBounds = + List<Value> selectedBounds = new ArrayList<>(ruleMap.getValuesFromInputType(inputs.get(i), currentRuleIdentifiers)); - sortedBounds.sort(Comparator.comparing(Value::getBoundary)); - Value lastBound = null; - boolean foundSubsumption = false; - for (Value currentBound : sortedBounds) { - if (lastBound != null) { - boolean isSubsumption = - currentBound.getBoundary().isBoundarySubsumption(lastBound.getBoundary()); - if (isSubsumption - || currentBound.getBoundary().isBoundaryEquals(lastBound.getBoundary())) { - currentBounds.add(lastBound); - if (isSubsumption) { - foundSubsumption = true; + + List<Set<Value>> clusters = new ArrayList<>(); + List<Boolean> subsumptions = new ArrayList<>(); + List<Value> subsumptionValue = new ArrayList<>(); + if (currentRootSubsumptionElement != null) { + Value nValue = ruleMap.getValuesFromInputType(inputs.get(i), Collections.singletonList(currentRootSubsumptionElement.getRuleIdentifier())).get(0); + Set<Value> c1 = new HashSet<>(); + c1.add(nValue); + boolean foundSubsumption = false; + for (Value cb2 : selectedBounds) { + boolean fs = nValue.getBoundary().isBoundarySubsumptionOf(cb2.getBoundary()); + foundSubsumption |= fs; + if (fs) { + c1.add(cb2); + } + } + if (c1.size() > 1) { + clusters.add(c1); + subsumptions.add(foundSubsumption); + subsumptionValue.add(nValue); + } + } else { + for (Value cb1 : selectedBounds) { + Set<Value> c1 = new HashSet<>(); + c1.add(cb1); + boolean foundSubsumption = false; + for (Value cb2 : selectedBounds) { + boolean fs = cb1.getBoundary().isBoundarySubsumptionOf(cb2.getBoundary()); + foundSubsumption |= fs; + if (fs) { + c1.add(cb2); } - } else { - if (currentBounds.size() > 0) { - currentBounds.add(lastBound); - checkForSubsumptionRules( - inputs, - i + 1, - currentBounds.stream().map(Value::getRuleIdentifier).collect(Collectors.toList()), - foundSubsumption || hasSubsumption); + } + if (c1.size() > 1) { + clusters.add(c1); + subsumptions.add(foundSubsumption); + subsumptionValue.add(cb1); + } + } + } + // check for subsets, that includes other subsets + for (int x = 0; x < clusters.size(); x++) { + if (clusters.get(x) != null) { + Set<Value> c1 = clusters.get(x); + for (int y = 0; y < clusters.size(); y++) { + if (clusters.get(y) != null) { + Set<Value> c2 = clusters.get(y); + if (c1.containsAll(c2) && x != y) { + clusters.set(y, null); + } } - currentBounds.clear(); } } - lastBound = currentBound; } - if (currentBounds.size() > 0) { - currentBounds.add(lastBound); - checkForSubsumptionRules( - inputs, - i + 1, - currentBounds.stream().map(Value::getRuleIdentifier).collect(Collectors.toList()), - foundSubsumption || hasSubsumption); + + for (int x = 0; x < clusters.size(); x++) { + if (clusters.get(x) != null) { + Set<Value> vals = clusters.get(x); + checkForSubsumptionRules( + inputs, + i + 1, + vals.stream().map(Value::getRuleIdentifier).collect(Collectors.toList()), + subsumptions.get(x) || hasSubsumption, subsumptionValue.get(x)); + } } } } @Override - protected void afterVerifyDecision() {} + protected void afterVerifyDecision() { + } private String getMessageText(List<RuleIdentifier> currentRuleIdentifiers) { StringBuilder sb = new StringBuilder("Rules "); diff --git a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/Test.java b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/Test.java deleted file mode 100644 index 4d5d453e3a7134e2bde758b4e5895eb1286a0093..0000000000000000000000000000000000000000 --- a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/Test.java +++ /dev/null @@ -1,39 +0,0 @@ -package de.unikoblenz.fgbks.dmn.core.verifier; - -import de.unikoblenz.fgbks.dmn.core.models.RuleIdentifier; -import de.unikoblenz.fgbks.dmn.core.models.VerificationResult; -import de.unikoblenz.fgbks.dmn.core.models.VerifierType; -import org.camunda.bpm.dmn.engine.DmnDecision; -import org.camunda.bpm.dmn.engine.impl.DmnDecisionTableImpl; -import org.camunda.bpm.dmn.engine.impl.DmnDecisionTableRuleImpl; - -public class Test extends AbstractVerifier { - - public Test() { - super(VerifierType.Subsumption); - } - - @Override - protected void beforeVerifyDecision() {} - - @Override - protected void verifyDecision(DmnDecision d) { - DmnDecisionTableImpl table = (DmnDecisionTableImpl) d.getDecisionLogic(); - int c = 0; - VerificationResult.Builder vBuilder = - VerificationResult.getBuilder().withMessage(d.getName() + " Verification!!!"); - for (DmnDecisionTableRuleImpl rule : table.getRules()) { - vBuilder.addRule( - RuleIdentifier.getBuilder() - .withDecisionName(d.getName()) - .withDecisionKey(d.getKey()) - .withRowNumber(++c) - .withRuleId(rule.getId()) - .build()); - } - addVerification(vBuilder.build()); - } - - @Override - protected void afterVerifyDecision() {} -} diff --git a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/Boundary.java b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/Boundary.java index 237e60b7e76587ffa50b594ff6752d666aa7b998..23b6f26c246473d03468f498768f81b34c19328c 100644 --- a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/Boundary.java +++ b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/Boundary.java @@ -3,9 +3,11 @@ package de.unikoblenz.fgbks.dmn.core.verifier.helper; import static de.unikoblenz.fgbks.dmn.core.verifier.helper.Boundary.BoundType.EXCLUSIVE; import static de.unikoblenz.fgbks.dmn.core.verifier.helper.Boundary.BoundType.INCLUSIVE; +import de.unikoblenz.fgbks.base.builder.DefaultBuilder; import java.util.Objects; import java.util.Optional; +/** Class for manage boundary and intervals. Can also handle String values. */ public class Boundary implements Comparable<Boundary> { public enum BoundType { @@ -194,6 +196,14 @@ public class Boundary implements Comparable<Boundary> { && other.isNumberInRange(this.getUpperBound(), this.getUpperBoundType()); } + public boolean isBoundarySubsumptionOf(Boundary other) { + if (isBoundarySubsumption(other)) { + return isNumberInRange(other.getLowerBound(), other.getLowerBoundType()) + && isNumberInRange(other.getUpperBound(), other.getUpperBoundType()); + } + return false; + } + /** * Check, if a other Boundary is NOT equals and NOT overlapping and NO subsumption Example: * @@ -257,6 +267,32 @@ public class Boundary implements Comparable<Boundary> { return equals(other); } + /** + * Check, if a other Boundary is a subsumption (NOT equals and NO overlapping) Example: + * + * <blockquote> + * + * <pre> + * false: (overlapping) + * o------o + * o-----o + * false: (not overlapping) + * o---o + * o--o + * false: (subsumption) + * o--o + * o-o + * true: (equals) + * o---o + * o---o + * false: (subsumption) + * o------o + * o--o + * </blockquote></pre> + * + * @param o Boundary to check + * @return true or false + */ @Override public boolean equals(Object o) { if (this == o) { @@ -272,6 +308,12 @@ public class Boundary implements Comparable<Boundary> { && upperBoundType == boundary.upperBoundType; } + /** + * Create an new boundary, witch is lover than the current <br> + * >10 --> <=10 + * + * @return An Optional of the Boundary, if the current has a possible boundary, which is lower + */ public Optional<Boundary> getNewBoundaryLower() { if (lowerBound == Double.MIN_VALUE) { return Optional.empty(); @@ -285,6 +327,14 @@ public class Boundary implements Comparable<Boundary> { .build()); } + /** + * Create a new boundary between two existing ones<br> + * <10 & >20 --> [10..20] + * + * @param other {@link Boundary} + * @return An Optional of the new Boundary between the two given ones Return an empty Optional, if + * there is no interval in between + */ public Optional<Boundary> getNewBoundaryBetween(Boundary other) { if (!isBoundaryNotInContact(Objects.requireNonNull(other))) { return Optional.empty(); @@ -308,6 +358,12 @@ public class Boundary implements Comparable<Boundary> { } } + /** + * Create a new boundary, witch begins at the upper bound of the original <br> + * <10 --> >=10 + * + * @return An Optional of the Boundary if the current has a possible boundary, which is higher + */ public Optional<Boundary> getNewBoundaryUpper() { if (upperBound == Double.MAX_VALUE) { return Optional.empty(); @@ -321,10 +377,19 @@ public class Boundary implements Comparable<Boundary> { .build()); } + /** + * Create a new Boundary from two given ones. The Optional is empty, if the given two bounds are + * not direct followed by each other. <u>Example:</u><br> + * <code> + * [10..20] & >20 --> >=10<br> [10..20] & <10 --> <=20<br> [10..20] & <=10 --> empty + * Optional (Boundaries are overlapping)<br> + * </code> + * + * @param other the other boundary + */ public Optional<Boundary> combineWith(Boundary other) { - // include subsumtion and overlapping???? if (isBoundaryNotInContact(other)) { - if (this.upperBound == other.lowerBound) { + if (this.upperBound == other.lowerBound && this.upperBoundType != other.lowerBoundType) { return Optional.of( getBuilder() .withLowerBound(this.lowerBound) @@ -332,7 +397,8 @@ public class Boundary implements Comparable<Boundary> { .withUpperBound(other.upperBound) .withUpperBoundType(other.getUpperBoundType()) .build()); - } else if (this.lowerBound == other.upperBound) { + } else if (this.lowerBound == other.upperBound + && other.upperBoundType != this.lowerBoundType) { return Optional.of( getBuilder() .withLowerBound(other.lowerBound) @@ -352,6 +418,18 @@ public class Boundary implements Comparable<Boundary> { return Objects.hash(lowerBound, lowerBoundType, upperBound, upperBoundType); } + /** + * Parse a String to a boundary object.<br> + * <u>Possible forms:</u><br> + * <code> + * Interval: [x..y] (inclusive x and inclusive y) <br> Interval: ]x..y[ (exclusive x and exclusive + * y) <br> Max Val: <=x (inclusive x) <br> Max Val: <x (exclusive x) <br> Min Val: >=x + * (inclusive x) <br> Min Val: >x (exclusive x) <br>Equal: =x (inclusive x) <br> + * </code> + * + * @param expr String to parse + * @return a new Boundary object + */ public static Boundary parseBoundary(String expr) { Builder builder = getBuilder(); try { @@ -394,6 +472,14 @@ public class Boundary implements Comparable<Boundary> { return builder.build(); } + /** + * Parse a String value to a boundary object. The Value of the String equals the hashcode of the + * string value and has the form<br> + * <code>={stringValue.hashCode()}</code> + * + * @param stringValue the string to create the boundary + * @return a boundary + */ public static Boundary parseBoundaryFromStringValue(String stringValue) { int hash = Objects.requireNonNull(stringValue).hashCode(); return getBuilder() @@ -404,6 +490,13 @@ public class Boundary implements Comparable<Boundary> { .build(); } + /** + * Parse a String value to a boundary object. The Value of the String equals the hashcode of the + * string value and has the form<br> + * <code>={stringValue.hashCode()}</code> + * + * @return a boundary + */ public static Boundary parseBoundaryFromBooleanValue(boolean bolVal) { int val = bolVal ? 0 : 1; return getBuilder() @@ -414,6 +507,11 @@ public class Boundary implements Comparable<Boundary> { .build(); } + /** + * Creats a "wildcard" boundary. [-infinity..+infinity] + * + * @return a boundary + */ public static Boundary parseBoundaryFromNullValue() { // Wildcard return getBuilder() @@ -476,7 +574,7 @@ public class Boundary implements Comparable<Boundary> { return upperBoundType == EXCLUSIVE ? -1 : 1; } } - return Double.compare(o.upperBound, this.upperBound); + return Double.compare(this.upperBound, o.upperBound); } else { return lowerBoundType == INCLUSIVE ? -1 : 1; } @@ -492,49 +590,44 @@ public class Boundary implements Comparable<Boundary> { return new Boundary().new Builder(); } - public class Builder { - - private Builder() {} + public class Builder extends DefaultBuilder<Boundary> { public Builder withUpperBound(double bound) { - setUpperBound(bound); + value.setUpperBound(bound); return this; } public Builder withLowerBound(double bound) { - setLowerBound(bound); + value.setLowerBound(bound); return this; } public Builder withUpperBoundType(BoundType boundType) { - setUpperBoundType(boundType); + value.setUpperBoundType(boundType); return this; } public Builder withLowerBoundType(BoundType boundType) { - setLowerBoundType(boundType); + value.setLowerBoundType(boundType); return this; } - private Boundary validate() { - if (lowerBound == Double.MIN_VALUE) { - lowerBoundType = INCLUSIVE; + @Override + protected void validate() { + if (value.lowerBound == Double.MIN_VALUE) { + value.lowerBoundType = INCLUSIVE; } - if (upperBound == Double.MAX_VALUE) { - upperBoundType = INCLUSIVE; + if (value.upperBound == Double.MAX_VALUE) { + value.upperBoundType = INCLUSIVE; } - Objects.requireNonNull(lowerBoundType, "Lower bound type is null"); - Objects.requireNonNull(upperBoundType, "Upper bound type is null"); - if (lowerBound > upperBound && Double.MIN_VALUE != lowerBound) { + Objects.requireNonNull(value.lowerBoundType, "Lower bound type is null"); + Objects.requireNonNull(value.upperBoundType, "Upper bound type is null"); + if (value.lowerBound > value.upperBound && Double.MIN_VALUE != value.lowerBound) { throw new IllegalArgumentException( String.format( - "Lower bound %f is greater than upper bound %f.", lowerBound, upperBound)); + "Lower bound %f is greater than upper bound %f.", + value.lowerBound, value.upperBound)); } - return Boundary.this; - } - - public Boundary build() { - return validate(); } } } diff --git a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/InputType.java b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/InputType.java index 9e08eed4b8a54cfe7150bc4067c098f581bcf815..c7842ff9a6ac1696ea308f5cc46111696eb07de4 100644 --- a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/InputType.java +++ b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/InputType.java @@ -5,14 +5,15 @@ import org.camunda.bpm.dmn.engine.impl.DmnDecisionTableInputImpl; public class InputType extends Type { - private InputType(String descisionKey, String id, DataType dataType) { - super(descisionKey, id, dataType); + private InputType(String descisionKey, String id, DataType dataType, String expression) { + super(descisionKey, id, dataType, expression); } public InputType(DmnDecision d, DmnDecisionTableInputImpl input) { this( d.getKey(), input.getId(), - DataType.getTypeFromString(input.getExpression().getTypeDefinition().getTypeName())); + DataType.getTypeFromString(input.getExpression().getTypeDefinition().getTypeName()), + input.getExpression().getExpression()); } } diff --git a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/OutputType.java b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/OutputType.java index a73a34bcf96a8707788578a45c5555ad81390d78..77b355908120523eacbe713f99c66182e32bf98d 100644 --- a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/OutputType.java +++ b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/OutputType.java @@ -5,14 +5,15 @@ import org.camunda.bpm.dmn.engine.impl.DmnDecisionTableOutputImpl; public class OutputType extends Type { - private OutputType(String descisionKey, String id, DataType dataType) { - super(descisionKey, id, dataType); + private OutputType(String descisionKey, String id, DataType dataType, String expression) { + super(descisionKey, id, dataType, expression); } public OutputType(DmnDecision d, DmnDecisionTableOutputImpl output) { this( d.getKey(), output.getId(), - DataType.getTypeFromString(output.getTypeDefinition().getTypeName())); + DataType.getTypeFromString(output.getTypeDefinition().getTypeName()), + output.getOutputName()); } } diff --git a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/RuleMap.java b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/RuleMap.java index 047c60199af2a484f7a92f226b0f39bdb2de4f68..5f375971a9957696d3c6d7f41462e18663cb4aed 100644 --- a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/RuleMap.java +++ b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/RuleMap.java @@ -4,8 +4,11 @@ import de.unikoblenz.fgbks.dmn.core.models.RuleIdentifier; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -16,23 +19,35 @@ import org.camunda.bpm.dmn.engine.impl.DmnDecisionTableOutputImpl; import org.camunda.bpm.dmn.engine.impl.DmnDecisionTableRuleImpl; import org.camunda.bpm.dmn.engine.impl.DmnExpressionImpl; +/** Class for storing inputs and outputs from a DMNDecsion List */ public class RuleMap { private static final Logger LOGGER = Logger.getLogger(RuleMap.class.getSimpleName()); + private List<DmnDecision> dmnDecisions; private Map<InputType, List<Value>> inputs; private Map<OutputType, List<Value>> outputs; + private Set<String> dmnDecisionsKeys; - public RuleMap() { + private RuleMap(List<DmnDecision> dmnDecisions) { + this.dmnDecisions = new ArrayList<>(dmnDecisions); this.inputs = new HashMap<>(); this.outputs = new HashMap<>(); + this.dmnDecisionsKeys = new HashSet<>(); } - public static RuleMap createMapFromDMN(List<DmnDecision> decisionList) { - RuleMap ruleMap = new RuleMap(); + /** + * Create a Map for all inputs and all outputs out of a list of DMN Decisions + * + * @param decisionList List of DMNDecision @NotNull + * @return a {@link RuleMap} from the list of DMNDecision + */ + public static RuleMap createMapFromDmn(List<DmnDecision> decisionList) { + RuleMap ruleMap = new RuleMap(decisionList); for (DmnDecision d : decisionList) { if (d.isDecisionTable()) { + ruleMap.dmnDecisionsKeys.add(d.getKey()); DmnDecisionTableImpl table = (DmnDecisionTableImpl) d.getDecisionLogic(); - + // inputs int i = 0; for (DmnDecisionTableInputImpl tableInput : table.getInputs()) { InputType it = new InputType(d, tableInput); @@ -45,6 +60,7 @@ public class RuleMap { } i++; } + // outputs i = 0; for (DmnDecisionTableOutputImpl tableOutput : table.getOutputs()) { OutputType ot = new OutputType(d, tableOutput); @@ -62,6 +78,12 @@ public class RuleMap { return ruleMap; } + /** + * Return a list of all input values from a given type. + * + * @param type the given input type + * @return a list of all Values from the type + */ public List<Value> getValuesFromInputType(Type type) { if (!inputs.keySet().contains(type)) { return Collections.emptyList(); @@ -69,6 +91,13 @@ public class RuleMap { return new ArrayList<>(inputs.get(type)); } + /** + * Return a list of input values with the given type, filtered by the given rule identifiers + * + * @param type the given input type + * @param ruleIdentifiersFilter the rules to filter + * @return a list of given input types + */ public List<Value> getValuesFromInputType(Type type, List<RuleIdentifier> ruleIdentifiersFilter) { if (!inputs.keySet().contains(type)) { return Collections.emptyList(); @@ -83,6 +112,12 @@ public class RuleMap { .collect(Collectors.toList()); } + /** + * Return a list of all output values from a given type. + * + * @param type the given output type + * @return a list of all values from the given output type + */ public List<Value> getValuesFromOutputType(Type type) { if (!outputs.keySet().contains(type)) { return Collections.emptyList(); @@ -90,6 +125,13 @@ public class RuleMap { return new ArrayList<>(outputs.get(type)); } + /** + * Return a list of output values with the given type, filtered by the given rule identifiers + * + * @param type the given output type + * @param ruleIdentifiersFilter the rules to filter + * @return a list of all values from the given output type + */ public List<Value> getValuesFromOutputType( Type type, List<RuleIdentifier> ruleIdentifiersFilter) { if (!outputs.keySet().contains(type)) { @@ -105,6 +147,12 @@ public class RuleMap { .collect(Collectors.toList()); } + /** + * Return a list of output or input values with the given type + * + * @param type the given output or input type + * @return a list of all values from the given output or input type + */ public List<Value> getValuesFromType(Type type) { if (outputs.keySet().contains(type)) { return new ArrayList<>(outputs.get(type)); @@ -114,6 +162,14 @@ public class RuleMap { return Collections.emptyList(); } + /** + * Return a list of output or input values with the given type, filtered by the given rule + * identifiers + * + * @param type the given output or input type + * @param ruleIdentifiersFilter the rules to filter + * @return a list of all values from the given output or input type + */ public List<Value> getValuesFromType(Type type, List<RuleIdentifier> ruleIdentifiersFilter) { if (ruleIdentifiersFilter == null) { return getValuesFromType(type); @@ -134,25 +190,84 @@ public class RuleMap { return Collections.emptyList(); } + /** + * Return a Set of input types, witch belongs to a given decisionKey + * + * @param decisionKey the decisionKey + * @return a set of Input types + */ public Set<Type> getAllInputTypesFromDecisionKey(String decisionKey) { return inputs .keySet() .stream() - .filter(i -> i.getDescisionKey().equals(decisionKey)) + .filter(i -> i.getDecisionKey().equals(Objects.requireNonNull(decisionKey))) .collect(Collectors.toSet()); } + /** + * Return a Set of output types, witch belongs to a given decisionKey + * + * @param decisionKey the decisionKey + * @return a set of Output types + */ public Set<Type> getAllOutputTypesFromDecisionKey(String decisionKey) { return outputs .keySet() .stream() - .filter(i -> i.getDescisionKey().equals(decisionKey)) + .filter(i -> i.getDecisionKey().equals(decisionKey)) .collect(Collectors.toSet()); } + /** + * Return a Set of input and output types, witch belongs to a given decisionKey + * + * @param decisionKey the decisionKey + * @return a set of output and input types + */ public Set<Type> getAllTypesFromDecisionKey(String decisionKey) { Set<Type> types = getAllInputTypesFromDecisionKey(decisionKey); types.addAll(getAllOutputTypesFromDecisionKey(decisionKey)); return types; } + + public Set<Set<String>> getIncludingInputDefinitions() { + Set<Set<String>> returnSet = new HashSet<>(); + for (String d1 : dmnDecisionsKeys) { + Set<String> subset = new HashSet<>(); + Set<Type> inputsD1 = getAllInputTypesFromDecisionKey(d1); + for (String d2 : dmnDecisionsKeys) { + Set<Type> inputsD2 = getAllInputTypesFromDecisionKey(d2); + boolean containsAll = true; + for (Type i1 : inputsD1) { + boolean f2 = false; + for (Type i2 : inputsD2) { + if (i1.getExpression() != null && i1.getExpression().equals(i2.getExpression())) { + f2 = true; + break; + } + } + if (!(containsAll &= f2)) { + break; + } + } + if (containsAll) { + subset.add(d2); + } + } + if (subset.size() > 1) { + returnSet.add(subset); + } + } + // check for subsets, that includes other subsets + Iterator<Set<String>> it = returnSet.iterator(); + while (it.hasNext()) { + Set<String> s = it.next(); + for (Set<String> subset : returnSet) { + if (subset.size() > s.size() && subset.containsAll(s)) { + it.remove(); + } + } + } + return returnSet; + } } diff --git a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/Type.java b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/Type.java index f1ef1f69652b43284a9d452a310d3eabb1dfc054..43819482edc0fcc89f0c992341f6744164d5060c 100644 --- a/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/Type.java +++ b/dmn-verifier-app/src/main/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/Type.java @@ -7,12 +7,13 @@ import org.camunda.bpm.dmn.engine.impl.DmnExpressionImpl; public abstract class Type { - protected final String descisionKey; + protected final String decisionKey; protected final String id; protected final DataType dataType; + protected final String expression; - public String getDescisionKey() { - return descisionKey; + public String getDecisionKey() { + return decisionKey; } public String getId() { @@ -23,10 +24,15 @@ public abstract class Type { return dataType; } - protected Type(String descisionKey, String id, DataType dataType) { - this.descisionKey = descisionKey; + public String getExpression() { + return expression; + } + + protected Type(String decisionKey, String id, DataType dataType, String expression) { + this.decisionKey = decisionKey; this.id = id; this.dataType = dataType; + this.expression = expression; } public Value getValue( @@ -55,10 +61,20 @@ public abstract class Type { return Boundary.parseBoundaryFromBooleanValue(Boolean.valueOf(value.getExpression())); case STRING: return Boundary.parseBoundaryFromStringValue(value.getExpression()); + case UNKNOWN: + case DATE: + throw new IllegalStateException( + String.format( + "Parsing boundary in table %s failed. Value: %s", + decisionKey, value.getExpression())); } return null; } + public boolean isEqualExpression(Type type) { + return this.expression.equals(type.expression); + } + @Override public boolean equals(Object o) { if (this == o) { @@ -68,17 +84,11 @@ public abstract class Type { return false; } Type type = (Type) o; - return descisionKey.equals(type.descisionKey) - && id.equals(type.id) - && dataType == type.dataType; + return decisionKey.equals(type.decisionKey) && id.equals(type.id) && dataType == type.dataType; } @Override public int hashCode() { - return Objects.hash(descisionKey, id, dataType); - } - - public String getCombinedKey() { - return descisionKey + id; + return Objects.hash(decisionKey, id, dataType); } } diff --git a/dmn-verifier-app/src/main/resources/sampleDMN.dmn b/dmn-verifier-app/src/main/resources/sampleDMN.dmn index 303b1f47da76775601207909507db44fdef5099d..a7c2bdd24682a55b6dcedd8b80bc2ec5e4505a1c 100644 --- a/dmn-verifier-app/src/main/resources/sampleDMN.dmn +++ b/dmn-verifier-app/src/main/resources/sampleDMN.dmn @@ -2,17 +2,17 @@ <definitions xmlns="http://www.omg.org/spec/DMN/20151101/dmn.xsd" xmlns:biodi="http://bpmn.io/schema/dmn/biodi/1.0" id="Definitions_1kjh9a2" name="DRD" namespace="http://camunda.org/schema/1.0/dmn"> <decision id="Decision_13nychf" name="Identical test"> <extensionElements> - <biodi:bounds x="120" y="145" width="180" height="80" /> + <biodi:bounds x="121" y="77" width="180" height="80" /> </extensionElements> <decisionTable id="decisionTable_1"> <input id="input_1"> <inputExpression id="inputExpression_1" typeRef="integer"> - <text>Testnumber</text> + <text>testNumber</text> </inputExpression> </input> <input id="InputClause_0rrhtk1"> <inputExpression id="LiteralExpression_16k88l6" typeRef="string"> - <text></text> + <text>testString</text> </inputExpression> </input> <output id="output_1" typeRef="string" /> @@ -249,4 +249,60 @@ </rule> </decisionTable> </decision> + <decision id="Decision_0daxsth" name="Overlapping rules"> + <extensionElements> + <biodi:bounds x="768" y="132" width="180" height="80" /> + </extensionElements> + <decisionTable id="DecisionTable_0q2corq"> + <input id="InputClause_0l9m2o0"> + <inputExpression id="LiteralExpression_1srvac8" typeRef="integer" /> + </input> + <output id="OutputClause_02zjru8" typeRef="string" /> + <rule id="DecisionRule_0tlti3q"> + <inputEntry id="UnaryTests_1epuvm6"> + <text><=20</text> + </inputEntry> + <outputEntry id="LiteralExpression_0p41xhl"> + <text></text> + </outputEntry> + </rule> + <rule id="DecisionRule_1bwl4r7"> + <inputEntry id="UnaryTests_1bvpeoa"> + <text>>=20</text> + </inputEntry> + <outputEntry id="LiteralExpression_1k69z44"> + <text></text> + </outputEntry> + </rule> + </decisionTable> + </decision> + <decision id="Decision_1mjix3c" name="Identical test 2"> + <extensionElements> + <biodi:bounds x="120" y="189" width="180" height="80" /> + </extensionElements> + <decisionTable id="DecisionTable_1ndo00u"> + <input id="InputClause_0ullwt4"> + <inputExpression id="LiteralExpression_0at3xrm" typeRef="integer"> + <text>testNumber</text> + </inputExpression> + </input> + <input id="InputClause_0wjsaiy"> + <inputExpression id="LiteralExpression_0kvzk24" typeRef="string"> + <text>testString</text> + </inputExpression> + </input> + <output id="OutputClause_1h8uwwl" typeRef="string" /> + <rule id="DecisionRule_06iq2ey"> + <inputEntry id="UnaryTests_0pt1lz0"> + <text>=10</text> + </inputEntry> + <inputEntry id="UnaryTests_1vaw0zi"> + <text>"Hallo"</text> + </inputEntry> + <outputEntry id="LiteralExpression_09yp5x6"> + <text></text> + </outputEntry> + </rule> + </decisionTable> + </decision> </definitions> diff --git a/dmn-verifier-app/src/test/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/wordnet/WordnetServiceTest.java b/dmn-verifier-app/src/test/java/de/unikoblenz/fgbks/base/wordnet/WordnetServiceTest.java similarity index 80% rename from dmn-verifier-app/src/test/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/wordnet/WordnetServiceTest.java rename to dmn-verifier-app/src/test/java/de/unikoblenz/fgbks/base/wordnet/WordnetServiceTest.java index fce6f9ffa68ef3c8e56423bb5004693b81a0a5f0..176d7567c5b4997b0d2f4f0994481809bc36466f 100644 --- a/dmn-verifier-app/src/test/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/wordnet/WordnetServiceTest.java +++ b/dmn-verifier-app/src/test/java/de/unikoblenz/fgbks/base/wordnet/WordnetServiceTest.java @@ -1,4 +1,4 @@ -package de.unikoblenz.fgbks.dmn.core.verifier.helper.wordnet; +package de.unikoblenz.fgbks.base.wordnet; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -7,7 +7,7 @@ import org.junit.jupiter.api.Test; class WordnetServiceTest { @Test - void areNounsSynonyms() { + void areNounsSynonymsTest() { WordnetService wns = WordnetService.getInstance(); assertTrue(wns.areNounsSynonyms("bill", "invoice")); assertTrue(wns.areNounsSynonyms("exam", "examination")); diff --git a/dmn-verifier-app/src/test/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/BoundaryTest.java b/dmn-verifier-app/src/test/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/BoundaryTest.java index 3d329ed9cf1d203996a285855848814ca32af248..3b18e6a83c1aeb83059d4e670f1785b20e4abb5a 100644 --- a/dmn-verifier-app/src/test/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/BoundaryTest.java +++ b/dmn-verifier-app/src/test/java/de/unikoblenz/fgbks/dmn/core/verifier/helper/BoundaryTest.java @@ -231,6 +231,11 @@ class BoundaryTest { b2 = Boundary.parseBoundary("[-10..100["); assertFalse(b1.isBoundaryNotInContact(b2)); assertFalse(b2.isBoundaryNotInContact(b1)); + + b1 = Boundary.parseBoundary("[0..10]"); + b2 = Boundary.parseBoundaryFromNullValue(); + assertFalse(b1.isBoundaryNotInContact(b2)); + assertFalse(b2.isBoundaryNotInContact(b1)); } @Test @@ -644,7 +649,7 @@ class BoundaryTest { assertEquals(b3, b1.combineWith(b2).get()); assertEquals(b3, b2.combineWith(b1).get()); - b1 = Boundary.parseBoundary("]0..10["); + b1 = Boundary.parseBoundary("[0..10["); b2 = Boundary.parseBoundary("<0"); b3 = Boundary.parseBoundary("<10"); assertEquals(b3, b1.combineWith(b2).get());