1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.databind.model.impl;
7   
8   import java.util.Arrays;
9   import java.util.regex.Pattern;
10  
11  import dev.metaschema.core.datatype.DataTypeService;
12  import dev.metaschema.core.datatype.IDataTypeAdapter;
13  import dev.metaschema.core.datatype.markup.MarkupLine;
14  import dev.metaschema.core.datatype.markup.MarkupMultiline;
15  import dev.metaschema.core.metapath.IMetapathExpression;
16  import dev.metaschema.core.model.ISource;
17  import dev.metaschema.core.model.constraint.AbstractConfigurableMessageConstraintBuilder;
18  import dev.metaschema.core.model.constraint.AbstractConstraintBuilder;
19  import dev.metaschema.core.model.constraint.AbstractKeyConstraintBuilder;
20  import dev.metaschema.core.model.constraint.IAllowedValue;
21  import dev.metaschema.core.model.constraint.IAllowedValuesConstraint;
22  import dev.metaschema.core.model.constraint.ICardinalityConstraint;
23  import dev.metaschema.core.model.constraint.IConstraint;
24  import dev.metaschema.core.model.constraint.IExpectConstraint;
25  import dev.metaschema.core.model.constraint.IIndexConstraint;
26  import dev.metaschema.core.model.constraint.IIndexHasKeyConstraint;
27  import dev.metaschema.core.model.constraint.IKeyField;
28  import dev.metaschema.core.model.constraint.ILet;
29  import dev.metaschema.core.model.constraint.IMatchesConstraint;
30  import dev.metaschema.core.model.constraint.IReportConstraint;
31  import dev.metaschema.core.model.constraint.IUniqueConstraint;
32  import dev.metaschema.core.util.ObjectUtils;
33  import dev.metaschema.databind.model.annotations.AllowedValue;
34  import dev.metaschema.databind.model.annotations.AllowedValues;
35  import dev.metaschema.databind.model.annotations.Expect;
36  import dev.metaschema.databind.model.annotations.HasCardinality;
37  import dev.metaschema.databind.model.annotations.Index;
38  import dev.metaschema.databind.model.annotations.IndexHasKey;
39  import dev.metaschema.databind.model.annotations.IsUnique;
40  import dev.metaschema.databind.model.annotations.KeyField;
41  import dev.metaschema.databind.model.annotations.Let;
42  import dev.metaschema.databind.model.annotations.Matches;
43  import dev.metaschema.databind.model.annotations.ModelUtil;
44  import dev.metaschema.databind.model.annotations.NullJavaTypeAdapter;
45  import dev.metaschema.databind.model.annotations.Property;
46  import dev.metaschema.databind.model.annotations.Report;
47  import edu.umd.cs.findbugs.annotations.NonNull;
48  import edu.umd.cs.findbugs.annotations.Nullable;
49  
50  @SuppressWarnings("PMD.CouplingBetweenObjects")
51  final class ConstraintFactory {
52    private ConstraintFactory() {
53      // disable
54    }
55  
56    static MarkupMultiline toRemarks(@NonNull String remarks) {
57      return remarks.isBlank() ? null : MarkupMultiline.fromMarkdown(remarks);
58    }
59  
60    @NonNull
61    static IMetapathExpression metapath(
62        @NonNull String metapath,
63        @NonNull ISource source) {
64      return metapath.isBlank()
65          ? IConstraint.defaultTarget()
66          : IMetapathExpression.lazyCompile(metapath, source.getStaticContext());
67    }
68  
69    @NonNull
70    static <T extends AbstractConstraintBuilder<T, ?>> T applyId(@NonNull T builder, @NonNull String id) {
71      if (!id.isBlank()) {
72        builder.identifier(id);
73      }
74      return builder;
75    }
76  
77    @NonNull
78    static <T extends AbstractConstraintBuilder<T, ?>> T applyFormalName(@NonNull T builder, @NonNull String name) {
79      if (!name.isBlank()) {
80        builder.formalName(name);
81      }
82      return builder;
83    }
84  
85    @NonNull
86    static <T extends AbstractConstraintBuilder<T, ?>> T applyDescription(@NonNull T builder, @NonNull String value) {
87      if (!value.isBlank()) {
88        builder.description(MarkupLine.fromMarkdown(value));
89      }
90      return builder;
91    }
92  
93    @NonNull
94    static <T extends AbstractConstraintBuilder<T, ?>> T applyTarget(
95        @NonNull T builder,
96        @NonNull IMetapathExpression expression) {
97      builder.target(expression);
98      return builder;
99    }
100 
101   @NonNull
102   static <T extends AbstractConstraintBuilder<T, ?>> T applyProperties(
103       @NonNull T builder,
104       @Nullable Property... properties) {
105     if (properties != null) {
106       Arrays.stream(properties)
107           .map(ModelUtil::toPropertyEntry)
108           .forEachOrdered(entry -> builder.property(
109               ObjectUtils.notNull(entry.getKey()),
110               ObjectUtils.notNull(entry.getValue())));
111     }
112     return builder;
113   }
114 
115   static <T extends AbstractConfigurableMessageConstraintBuilder<T, ?>> T applyMessage(@NonNull T builder,
116       @Nullable String message) {
117     if (message != null && !message.isBlank()) {
118       builder.message(message);
119     }
120     return builder;
121   }
122 
123   static <T extends AbstractConstraintBuilder<T, ?>> T applyRemarks(@NonNull T builder, @NonNull String remarks) {
124     if (!remarks.isBlank()) {
125       builder.remarks(MarkupMultiline.fromMarkdown(remarks));
126     }
127     return builder;
128   }
129 
130   @SuppressWarnings("PMD.NullAssignment")
131   @NonNull
132   static IAllowedValuesConstraint.Builder applyAllowedValues(
133       @NonNull IAllowedValuesConstraint.Builder builder,
134       @NonNull AllowedValues constraint) {
135     for (AllowedValue value : constraint.values()) {
136       String deprecatedVersion = value.deprecatedVersion();
137       if (deprecatedVersion.isBlank()) {
138         deprecatedVersion = null;
139       }
140 
141       IAllowedValue allowedValue = IAllowedValue.of(
142           value.value(),
143           MarkupLine.fromMarkdown(value.description()),
144           deprecatedVersion);
145       builder.allowedValue(allowedValue);
146     }
147     return builder;
148   }
149 
150   @Nullable
151   static Pattern toPattern(@NonNull String pattern) {
152     return pattern.isBlank() ? null : Pattern.compile(pattern);
153   }
154 
155   @Nullable
156   static String toMessage(@NonNull String message) {
157     return message.isBlank() ? null : message;
158   }
159 
160   @Nullable
161   static IDataTypeAdapter<?> toDataType(@NonNull Class<? extends IDataTypeAdapter<?>> adapterClass) {
162     return adapterClass.isAssignableFrom(NullJavaTypeAdapter.class) ? null
163         : DataTypeService.instance().getDataTypeByAdapterClass(adapterClass);
164   }
165 
166   @NonNull
167   static IAllowedValuesConstraint newAllowedValuesConstraint(
168       @NonNull AllowedValues constraint,
169       @NonNull ISource source) {
170     IAllowedValuesConstraint.Builder builder = IAllowedValuesConstraint.builder();
171     applyId(builder, constraint.id());
172     applyFormalName(builder, constraint.formalName());
173     applyDescription(builder, constraint.description());
174     builder
175         .source(source)
176         .level(constraint.level());
177     applyTarget(builder, metapath(constraint.target(), source));
178     applyProperties(builder, constraint.properties());
179     applyRemarks(builder, constraint.remarks());
180 
181     applyAllowedValues(builder, constraint);
182     builder.allowsOther(constraint.allowOthers());
183     builder.extensible(constraint.extensible());
184 
185     return builder.build();
186   }
187 
188   @NonNull
189   static IMatchesConstraint newMatchesConstraint(Matches constraint, @NonNull ISource source) {
190     IMatchesConstraint.Builder builder = IMatchesConstraint.builder();
191     applyId(builder, constraint.id());
192     applyFormalName(builder, constraint.formalName());
193     applyDescription(builder, constraint.description());
194     builder
195         .source(source)
196         .level(constraint.level());
197     applyTarget(builder, metapath(constraint.target(), source));
198     applyProperties(builder, constraint.properties());
199     applyMessage(builder, constraint.message());
200     applyRemarks(builder, constraint.remarks());
201 
202     Pattern pattern = toPattern(constraint.pattern());
203     if (pattern != null) {
204       builder.regex(pattern);
205     }
206 
207     IDataTypeAdapter<?> dataType = toDataType(constraint.typeAdapter());
208     if (dataType != null) {
209       builder.datatype(dataType);
210     }
211 
212     return builder.build();
213   }
214 
215   @NonNull
216   static <T extends AbstractKeyConstraintBuilder<T, ?>> T applyKeyFields(
217       @NonNull T builder,
218       @NonNull ISource source,
219       @NonNull KeyField... keyFields) {
220     for (KeyField keyField : keyFields) {
221       IKeyField field = IKeyField.of(
222           metapath(keyField.target(), source),
223           toPattern(keyField.pattern()),
224           toRemarks(keyField.remarks()));
225       builder.keyField(field);
226     }
227     return builder;
228   }
229 
230   @NonNull
231   static IUniqueConstraint newUniqueConstraint(@NonNull IsUnique constraint, @NonNull ISource source) {
232     IUniqueConstraint.Builder builder = IUniqueConstraint.builder();
233     applyId(builder, constraint.id());
234     applyFormalName(builder, constraint.formalName());
235     applyDescription(builder, constraint.description());
236     builder
237         .source(source)
238         .level(constraint.level());
239     applyTarget(builder, metapath(constraint.target(), source));
240     applyProperties(builder, constraint.properties());
241     applyMessage(builder, constraint.message());
242     applyRemarks(builder, constraint.remarks());
243 
244     applyKeyFields(builder, source, constraint.keyFields());
245 
246     return builder.build();
247   }
248 
249   @NonNull
250   static IIndexConstraint newIndexConstraint(@NonNull Index constraint, @NonNull ISource source) {
251     IIndexConstraint.Builder builder = IIndexConstraint.builder(constraint.name());
252     applyId(builder, constraint.id());
253     applyFormalName(builder, constraint.formalName());
254     applyDescription(builder, constraint.description());
255     builder
256         .source(source)
257         .level(constraint.level());
258     applyTarget(builder, metapath(constraint.target(), source));
259     applyProperties(builder, constraint.properties());
260     applyMessage(builder, constraint.message());
261     applyRemarks(builder, constraint.remarks());
262 
263     applyKeyFields(builder, source, constraint.keyFields());
264 
265     return builder.build();
266   }
267 
268   @NonNull
269   static IIndexHasKeyConstraint newIndexHasKeyConstraint(
270       @NonNull IndexHasKey constraint,
271       @NonNull ISource source) {
272     IIndexHasKeyConstraint.Builder builder = IIndexHasKeyConstraint.builder(constraint.indexName());
273     applyId(builder, constraint.id());
274     applyFormalName(builder, constraint.formalName());
275     applyDescription(builder, constraint.description());
276     builder
277         .source(source)
278         .level(constraint.level());
279     applyTarget(builder, metapath(constraint.target(), source));
280     applyProperties(builder, constraint.properties());
281     applyMessage(builder, constraint.message());
282     applyRemarks(builder, constraint.remarks());
283 
284     applyKeyFields(builder, source, constraint.keyFields());
285 
286     return builder.build();
287   }
288 
289   @NonNull
290   static IExpectConstraint newExpectConstraint(@NonNull Expect constraint, @NonNull ISource source) {
291     IExpectConstraint.Builder builder = IExpectConstraint.builder();
292     applyId(builder, constraint.id());
293     applyFormalName(builder, constraint.formalName());
294     applyDescription(builder, constraint.description());
295     builder
296         .source(source)
297         .level(constraint.level());
298     applyTarget(builder, metapath(constraint.target(), source));
299     applyProperties(builder, constraint.properties());
300     applyMessage(builder, constraint.message());
301     applyRemarks(builder, constraint.remarks());
302 
303     builder.test(metapath(constraint.test(), source));
304 
305     return builder.build();
306   }
307 
308   /**
309    * Create a new report constraint from the provided annotation.
310    * <p>
311    * Report constraints generate findings when their test expression evaluates to
312    * {@code true}, which is the opposite of expect constraints.
313    *
314    * @param constraint
315    *          the annotation containing the constraint configuration
316    * @param source
317    *          the source of the constraint
318    * @return a new report constraint
319    */
320   @NonNull
321   static IReportConstraint newReportConstraint(@NonNull Report constraint, @NonNull ISource source) {
322     IReportConstraint.Builder builder = IReportConstraint.builder();
323     applyId(builder, constraint.id());
324     applyFormalName(builder, constraint.formalName());
325     applyDescription(builder, constraint.description());
326     builder
327         .source(source)
328         .level(constraint.level());
329     applyTarget(builder, metapath(constraint.target(), source));
330     applyProperties(builder, constraint.properties());
331     applyMessage(builder, constraint.message());
332     applyRemarks(builder, constraint.remarks());
333 
334     builder.test(metapath(constraint.test(), source));
335 
336     return builder.build();
337   }
338 
339   @Nullable
340   static Integer toCardinality(int value) {
341     return value < 0 ? null : value;
342   }
343 
344   @NonNull
345   static ICardinalityConstraint newCardinalityConstraint(@NonNull HasCardinality constraint,
346       @NonNull ISource source) {
347     ICardinalityConstraint.Builder builder = ICardinalityConstraint.builder();
348     applyId(builder, constraint.id());
349     applyFormalName(builder, constraint.formalName());
350     applyDescription(builder, constraint.description());
351     builder
352         .source(source)
353         .level(constraint.level());
354     applyTarget(builder, metapath(constraint.target(), source));
355     applyProperties(builder, constraint.properties());
356     applyMessage(builder, constraint.message());
357     applyRemarks(builder, constraint.remarks());
358 
359     Integer min = toCardinality(constraint.minOccurs());
360     if (min != null) {
361       builder.minOccurs(min);
362     }
363     Integer max = toCardinality(constraint.maxOccurs());
364     if (max != null) {
365       builder.maxOccurs(max);
366     }
367 
368     return builder.build();
369   }
370 
371   @NonNull
372   static ILet newLetExpression(@NonNull Let annotation, @NonNull ISource source) {
373     String remarkMarkdown = annotation.remarks();
374     MarkupMultiline remarks = remarkMarkdown.isBlank()
375         ? null
376         : MarkupMultiline.fromMarkdown(remarkMarkdown);
377     return ILet.of(
378         source.getStaticContext().parseVariableName(annotation.name()),
379         IMetapathExpression.lazyCompile(annotation.target(), source.getStaticContext()),
380         source,
381         remarks);
382   }
383 }