1
2
3
4
5
6 package dev.metaschema.cli.commands;
7
8 import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
9 import com.fasterxml.jackson.dataformat.yaml.YAMLFactoryBuilder;
10 import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
11
12 import org.apache.commons.cli.CommandLine;
13 import org.apache.commons.cli.Option;
14 import org.apache.logging.log4j.LogManager;
15 import org.apache.logging.log4j.Logger;
16
17 import java.io.IOException;
18 import java.io.PrintWriter;
19 import java.io.StringWriter;
20 import java.io.Writer;
21 import java.net.URI;
22 import java.net.URISyntaxException;
23 import java.nio.charset.StandardCharsets;
24 import java.nio.file.Files;
25 import java.nio.file.Path;
26 import java.nio.file.StandardOpenOption;
27 import java.util.Collection;
28 import java.util.Comparator;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Set;
32 import java.util.TreeMap;
33 import java.util.function.Function;
34 import java.util.stream.Collectors;
35
36 import dev.metaschema.cli.processor.CallingContext;
37 import dev.metaschema.cli.processor.ExitCode;
38 import dev.metaschema.cli.processor.command.AbstractTerminalCommand;
39 import dev.metaschema.cli.processor.command.CommandExecutionException;
40 import dev.metaschema.cli.processor.command.ExtraArgument;
41 import dev.metaschema.cli.processor.command.ICommandExecutor;
42 import dev.metaschema.core.metapath.DynamicContext;
43 import dev.metaschema.core.metapath.StaticContext;
44 import dev.metaschema.core.metapath.item.node.AllowedValueCollectingNodeItemVisitor;
45 import dev.metaschema.core.metapath.item.node.AllowedValueCollectingNodeItemVisitor.AllowedValuesRecord;
46 import dev.metaschema.core.metapath.item.node.IDefinitionNodeItem;
47 import dev.metaschema.core.metapath.item.node.IModuleNodeItem;
48 import dev.metaschema.core.metapath.item.node.INodeItemFactory;
49 import dev.metaschema.core.model.IModule;
50 import dev.metaschema.core.model.constraint.IAllowedValue;
51 import dev.metaschema.core.model.constraint.IAllowedValuesConstraint;
52 import dev.metaschema.core.model.constraint.IConstraintSet;
53 import dev.metaschema.core.util.ObjectUtils;
54 import dev.metaschema.databind.IBindingContext;
55 import edu.umd.cs.findbugs.annotations.NonNull;
56
57
58
59
60
61
62
63
64
65 @SuppressWarnings("PMD.CouplingBetweenObjects")
66 public class ListAllowedValuesCommand
67 extends AbstractTerminalCommand {
68 private static final Logger LOGGER = LogManager.getLogger(ListAllowedValuesCommand.class);
69
70 @NonNull
71 private static final String COMMAND = "list-allowed-values";
72 @NonNull
73 private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
74 ExtraArgument.newInstance("metaschema-module-file-or-URL", true),
75 ExtraArgument.newInstance("destination-file", false)));
76 @NonNull
77 private static final Option CONSTRAINTS_OPTION = ObjectUtils.notNull(
78 Option.builder("c")
79 .hasArgs()
80 .argName("URL")
81 .desc("additional constraint definitions")
82 .get());
83
84 @Override
85 public String getName() {
86 return COMMAND;
87 }
88
89 @Override
90 public String getDescription() {
91 return "List allowed values constraints for the provided Metaschema module";
92 }
93
94 @SuppressWarnings("null")
95 @Override
96 public Collection<? extends Option> gatherOptions() {
97 return List.of(
98 CONSTRAINTS_OPTION,
99 MetaschemaCommands.OVERWRITE_OPTION);
100 }
101
102 @Override
103 public List<ExtraArgument> getExtraArguments() {
104 return EXTRA_ARGUMENTS;
105 }
106
107 @Override
108 public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) {
109 return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand);
110 }
111
112
113
114
115
116
117
118
119
120
121
122 @SuppressWarnings({
123 "PMD.OnlyOneReturn",
124 "PMD.AvoidCatchingGenericException",
125 "PMD.CognitiveComplexity"
126 })
127 protected void executeCommand(
128 @NonNull CallingContext callingContext,
129 @NonNull CommandLine cmdLine) throws CommandExecutionException {
130
131 List<String> extraArgs = cmdLine.getArgList();
132
133 Path destination = null;
134 if (extraArgs.size() > 1) {
135 destination = MetaschemaCommands.handleDestination(ObjectUtils.requireNonNull(extraArgs.get(1)), cmdLine);
136 }
137
138 URI currentWorkingDirectory = ObjectUtils.notNull(getCurrentWorkingDirectory().toUri());
139 Set<IConstraintSet> constraintSets = MetaschemaCommands.loadConstraintSets(
140 cmdLine,
141 CONSTRAINTS_OPTION,
142 currentWorkingDirectory);
143
144 IBindingContext bindingContext = MetaschemaCommands.newBindingContextWithDynamicCompilation(constraintSets);
145
146 URI moduleUri;
147 try {
148 moduleUri = resolveAgainstCWD(ObjectUtils.requireNonNull(extraArgs.get(0)));
149 } catch (URISyntaxException ex) {
150 throw new CommandExecutionException(
151 ExitCode.INVALID_ARGUMENTS,
152 String.format("Cannot load module as '%s' is not a valid file or URL. %s",
153 extraArgs.get(0),
154 ex.getLocalizedMessage()),
155 ex);
156 }
157 IModule module = MetaschemaCommands.loadModule(moduleUri, bindingContext);
158
159 try {
160 if (destination == null) {
161 Writer stringWriter = new StringWriter();
162 try (PrintWriter writer = new PrintWriter(stringWriter)) {
163 generateAllowedValuesList(module, writer);
164 }
165
166 if (LOGGER.isInfoEnabled()) {
167 LOGGER.info(stringWriter.toString());
168 }
169 } else {
170 try (Writer writer = Files.newBufferedWriter(
171 destination,
172 StandardCharsets.UTF_8,
173 StandardOpenOption.CREATE,
174 StandardOpenOption.WRITE,
175 StandardOpenOption.TRUNCATE_EXISTING)) {
176 try (PrintWriter printWriter = new PrintWriter(writer)) {
177 generateAllowedValuesList(module, printWriter);
178 }
179 }
180 }
181 } catch (IOException ex) {
182 throw new CommandExecutionException(ExitCode.IO_ERROR, ex);
183 } catch (RuntimeException ex) {
184 throw new CommandExecutionException(ExitCode.RUNTIME_ERROR, ex);
185 }
186 }
187
188 private static void generateAllowedValuesList(
189 @NonNull IModule module,
190 @NonNull PrintWriter writer) throws IOException {
191 AllowedValueCollectingNodeItemVisitor walker = new AllowedValueCollectingNodeItemVisitor();
192
193 StaticContext staticContext = StaticContext.builder()
194 .defaultModelNamespace(module.getXmlNamespace())
195 .build();
196
197 IModuleNodeItem moduleNodeItem = INodeItemFactory.instance().newModuleNodeItem(module);
198
199 DynamicContext dynamicContext = new DynamicContext(staticContext);
200 dynamicContext.disablePredicateEvaluation();
201
202 walker.visit(moduleNodeItem, dynamicContext);
203
204 Map<IDefinitionNodeItem<?, ?>,
205 List<AllowedValuesRecord>> allowedValuesByTarget
206 = ObjectUtils.notNull(walker.getAllowedValueLocations().stream()
207 .flatMap(location -> location.getAllowedValues().stream())
208 .collect(Collectors.groupingBy(AllowedValuesRecord::getTarget,
209 () -> new TreeMap<>(Comparator.comparing(IDefinitionNodeItem::getMetapath)),
210 Collectors.mapping(Function.identity(), Collectors.toUnmodifiableList()))));
211
212 generateYaml(allowedValuesByTarget, writer);
213 }
214
215 private static void generateYaml(
216 @NonNull Map<IDefinitionNodeItem<?, ?>, List<AllowedValuesRecord>> allowedValuesByTarget,
217 @NonNull PrintWriter writer) throws IOException {
218
219 YAMLFactoryBuilder builder = YAMLFactory.builder();
220 YAMLFactory factory = ObjectUtils.notNull(builder
221 .enable(YAMLGenerator.Feature.MINIMIZE_QUOTES)
222 .enable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)
223 .enable(YAMLGenerator.Feature.ALWAYS_QUOTE_NUMBERS_AS_STRINGS)
224 .disable(YAMLGenerator.Feature.SPLIT_LINES)
225 .build());
226
227 try (YAMLGenerator generator = factory.createGenerator(writer)) {
228
229 generator.writeStartObject();
230
231 writeLocations(allowedValuesByTarget, generator);
232
233 generator.writeEndObject();
234 }
235 }
236
237 private static void writeLocations(
238 @NonNull Map<IDefinitionNodeItem<?, ?>, List<AllowedValuesRecord>> allowedValuesByTarget,
239 @NonNull YAMLGenerator generator) throws IOException {
240 generator.writeFieldName("locations");
241 generator.writeStartObject();
242
243 for (Map.Entry<IDefinitionNodeItem<?, ?>, List<AllowedValuesRecord>> entry : allowedValuesByTarget.entrySet()) {
244 assert entry != null;
245 writeLocation(entry, generator);
246 }
247 generator.writeEndObject();
248 }
249
250 private static void writeLocation(
251 @NonNull Map.Entry<IDefinitionNodeItem<?, ?>, List<AllowedValuesRecord>> entry,
252 @NonNull YAMLGenerator generator) throws IOException {
253
254 IDefinitionNodeItem<?, ?> target = ObjectUtils.notNull(entry.getKey());
255
256 generator.writeFieldName(metapath(target));
257
258 generator.writeStartObject();
259
260 writeLocationConstraints(entry, generator);
261
262 generator.writeEndObject();
263 }
264
265 private static void writeLocationConstraints(
266 @NonNull Map.Entry<IDefinitionNodeItem<?, ?>, List<AllowedValuesRecord>> entry,
267 @NonNull YAMLGenerator generator) throws IOException {
268 IDefinitionNodeItem<?, ?> target = ObjectUtils.notNull(entry.getKey());
269
270 List<AllowedValuesRecord> allowedValues = entry.getValue();
271 if (allowedValues != null) {
272 generator.writeFieldName("constraints");
273
274 generator.writeStartArray();
275
276 for (AllowedValuesRecord record : allowedValues) {
277 assert target.equals(record.getTarget());
278
279 writeAllowedValue(record, generator);
280 }
281
282 generator.writeEndArray();
283 }
284 }
285
286 private static void writeAllowedValue(@NonNull AllowedValuesRecord record, @NonNull YAMLGenerator generator)
287 throws IOException {
288
289 generator.writeStartObject();
290
291 generator.writeStringField("type", "allowed-values");
292
293 IAllowedValuesConstraint constraint = record.getAllowedValues();
294 if (constraint.getId() != null) {
295 generator.writeStringField("identifier", constraint.getId());
296 }
297 generator.writeStringField("location", metapath(record.getLocation()));
298 generator.writeStringField("target", constraint.getTarget().getPath());
299
300 List<String> values = constraint.getAllowedValues().values().stream()
301 .map(IAllowedValue::getValue)
302 .collect(Collectors.toList());
303 generator.writeFieldName("values");
304 if (values == null) {
305 generator.writeNull();
306 } else {
307 generator.writeStartArray();
308 for (String value : values) {
309 generator.writeString(value);
310 }
311 generator.writeEndArray();
312 }
313
314 generator.writeBooleanField("allow-other", constraint.isAllowedOther());
315
316 URI source = constraint.getSource().getSource();
317 generator.writeStringField("source", source == null ? "builtin" : source.toString());
318
319 generator.writeEndObject();
320 }
321
322 private static String metapath(@NonNull IDefinitionNodeItem<?, ?> item) {
323 return metapath(item.getMetapath());
324 }
325
326 private static String metapath(@NonNull String path) {
327
328 return path.replace("[1]", "");
329 }
330 }