1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.core.model.constraint.impl;
7   
8   import java.util.Map;
9   import java.util.Set;
10  import java.util.regex.Pattern;
11  
12  import dev.metaschema.core.datatype.markup.MarkupLine;
13  import dev.metaschema.core.datatype.markup.MarkupMultiline;
14  import dev.metaschema.core.metapath.DynamicContext;
15  import dev.metaschema.core.metapath.IMetapathExpression;
16  import dev.metaschema.core.metapath.InvalidMetapathGrammarException;
17  import dev.metaschema.core.metapath.MetapathException;
18  import dev.metaschema.core.metapath.item.node.INodeItem;
19  import dev.metaschema.core.model.IAttributable;
20  import dev.metaschema.core.model.ISource;
21  import dev.metaschema.core.model.constraint.ConstraintInitializationException;
22  import dev.metaschema.core.model.constraint.ConstraintValidationException;
23  import dev.metaschema.core.model.constraint.IConfigurableMessageConstraint;
24  import dev.metaschema.core.model.constraint.IConstraint;
25  import dev.metaschema.core.util.ObjectUtils;
26  import dev.metaschema.core.util.StringUtils;
27  import edu.umd.cs.findbugs.annotations.NonNull;
28  import edu.umd.cs.findbugs.annotations.Nullable;
29  
30  /**
31   * The base class for all constraint implementations that allow a configurable
32   * message.
33   *
34   * @since 2.0.0
35   */
36  public abstract class AbstractConfigurableMessageConstraint
37      extends AbstractConstraint
38      implements IConfigurableMessageConstraint {
39    @NonNull
40    private static final Pattern METAPATH_VALUE_TEMPLATE_PATTERN
41        = ObjectUtils.notNull(Pattern.compile("(?<!\\\\)(\\{\\s*((?:(?:\\\\})|[^}])*)\\s*\\})"));
42  
43    @Nullable
44    private final String message;
45  
46    /**
47     * Construct a new Metaschema constraint.
48     *
49     * @param id
50     *          the optional identifier for the constraint
51     * @param formalName
52     *          the constraint's formal name or {@code null} if not provided
53     * @param description
54     *          the constraint's semantic description or {@code null} if not
55     *          provided
56     * @param source
57     *          information about the constraint source
58     * @param level
59     *          the significance of a violation of this constraint
60     * @param target
61     *          the Metapath expression identifying the nodes the constraint targets
62     * @param properties
63     *          a collection of associated properties
64     * @param message
65     *          an optional message to emit when the constraint is violated
66     * @param remarks
67     *          optional remarks describing the intent of the constraint
68     */
69    protected AbstractConfigurableMessageConstraint(
70        @Nullable String id,
71        @Nullable String formalName,
72        @Nullable MarkupLine description,
73        @NonNull ISource source,
74        @NonNull Level level,
75        @NonNull IMetapathExpression target,
76        @NonNull Map<IAttributable.Key, Set<String>> properties,
77        @Nullable String message,
78        @Nullable MarkupMultiline remarks) {
79      super(id, formalName, description, source, level, target, properties, remarks);
80      this.message = message;
81    }
82  
83    @Override
84    public String getMessage() {
85      return message;
86    }
87  
88    @Override
89    public String generateMessage(@NonNull INodeItem item, @NonNull DynamicContext context)
90        throws ConstraintValidationException {
91      String message = getMessage();
92      if (message == null) {
93        throw new ConstraintInitializationException(
94            String.format("A custom message is not defined in the constraint %s in %s.",
95                IConstraint.getConstraintIdentity(this),
96                getSource().getLocationHint()));
97      }
98      try {
99        return ObjectUtils.notNull(StringUtils.replaceTokens(message, METAPATH_VALUE_TEMPLATE_PATTERN, match -> {
100         String metapath = ObjectUtils.notNull(match.group(2));
101         IMetapathExpression expr = IMetapathExpression.compile(
102             metapath,
103             // need to use the static context of the source to resolve prefixes, etc., since
104             // this is where the message is defined
105             getSource().getStaticContext());
106         return expr.evaluateAs(
107             item,
108             IMetapathExpression.ResultType.STRING,
109             // here we are using the static context of the instance, since this is how
110             // variables and nodes are resolved.
111             context);
112       }).toString());
113     } catch (InvalidMetapathGrammarException ex) {
114       throw new ConstraintValidationException(
115           String.format("Unable to compile a message replacement expression in constraint '%s'. %s",
116               IConstraint.getConstraintIdentity(this),
117               ex.getLocalizedMessage()),
118           ex);
119     } catch (MetapathException ex) {
120       throw new ConstraintValidationException(
121           String.format("Unable to evaluate a message replacement expression in constraint '%s'. %s",
122               IConstraint.getConstraintIdentity(this),
123               ex.getLocalizedMessage()),
124           ex);
125     }
126   }
127 }