1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 package gov.nist.secauto.metaschema.cli.commands;
28
29 import gov.nist.secauto.metaschema.cli.processor.CLIProcessor;
30 import gov.nist.secauto.metaschema.cli.processor.CLIProcessor.CallingContext;
31 import gov.nist.secauto.metaschema.cli.processor.ExitCode;
32 import gov.nist.secauto.metaschema.cli.processor.ExitStatus;
33 import gov.nist.secauto.metaschema.cli.processor.InvalidArgumentException;
34 import gov.nist.secauto.metaschema.cli.processor.OptionUtils;
35 import gov.nist.secauto.metaschema.cli.processor.command.AbstractCommandExecutor;
36 import gov.nist.secauto.metaschema.cli.processor.command.AbstractTerminalCommand;
37 import gov.nist.secauto.metaschema.cli.processor.command.DefaultExtraArgument;
38 import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument;
39 import gov.nist.secauto.metaschema.cli.util.LoggingValidationHandler;
40 import gov.nist.secauto.metaschema.core.model.MetaschemaException;
41 import gov.nist.secauto.metaschema.core.model.constraint.IConstraintSet;
42 import gov.nist.secauto.metaschema.core.model.validation.IValidationResult;
43 import gov.nist.secauto.metaschema.core.model.xml.ConstraintLoader;
44 import gov.nist.secauto.metaschema.core.util.CollectionUtil;
45 import gov.nist.secauto.metaschema.core.util.CustomCollectors;
46 import gov.nist.secauto.metaschema.core.util.ObjectUtils;
47 import gov.nist.secauto.metaschema.databind.IBindingContext;
48 import gov.nist.secauto.metaschema.databind.IBindingContext.IValidationSchemaProvider;
49 import gov.nist.secauto.metaschema.databind.io.Format;
50 import gov.nist.secauto.metaschema.databind.io.FormatDetector;
51 import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
52
53 import org.apache.commons.cli.CommandLine;
54 import org.apache.commons.cli.Option;
55 import org.apache.logging.log4j.LogManager;
56 import org.apache.logging.log4j.Logger;
57 import org.xml.sax.SAXException;
58
59 import java.io.FileNotFoundException;
60 import java.io.IOException;
61 import java.nio.file.Files;
62 import java.nio.file.Path;
63 import java.nio.file.Paths;
64 import java.util.Arrays;
65 import java.util.Collection;
66 import java.util.LinkedHashSet;
67 import java.util.List;
68 import java.util.Locale;
69 import java.util.Set;
70
71 import edu.umd.cs.findbugs.annotations.NonNull;
72
73 public abstract class AbstractValidateContentCommand
74 extends AbstractTerminalCommand {
75 private static final Logger LOGGER = LogManager.getLogger(AbstractValidateContentCommand.class);
76 @NonNull
77 private static final String COMMAND = "validate";
78 @NonNull
79 private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
80 new DefaultExtraArgument("file to validate", true)));
81
82 @NonNull
83 private static final Option AS_OPTION = ObjectUtils.notNull(
84 Option.builder()
85 .longOpt("as")
86 .hasArg()
87 .argName("FORMAT")
88 .desc("source format: xml, json, or yaml")
89 .build());
90 @NonNull
91 private static final Option CONSTRAINTS_OPTION = ObjectUtils.notNull(
92 Option.builder("c")
93 .hasArg()
94 .argName("FILE")
95 .desc("additional constraint definitions")
96 .build());
97
98 @Override
99 public String getName() {
100 return COMMAND;
101 }
102
103 @SuppressWarnings("null")
104 @Override
105 public Collection<? extends Option> gatherOptions() {
106 return List.of(
107 AS_OPTION,
108 CONSTRAINTS_OPTION);
109 }
110
111 @Override
112 public List<ExtraArgument> getExtraArguments() {
113 return EXTRA_ARGUMENTS;
114 }
115
116 @SuppressWarnings("PMD.PreserveStackTrace")
117 @Override
118 public void validateOptions(CallingContext callingContext, CommandLine cmdLine) throws InvalidArgumentException {
119 if (cmdLine.hasOption(CONSTRAINTS_OPTION)) {
120 String[] args = cmdLine.getOptionValues(CONSTRAINTS_OPTION);
121 for (String arg : args) {
122 Path constraint = Paths.get(arg);
123 if (!Files.exists(constraint)) {
124 throw new InvalidArgumentException(
125 "The provided external constraint file '" + constraint + "' does not exist.");
126 }
127 if (!Files.isRegularFile(constraint)) {
128 throw new InvalidArgumentException(
129 "The provided external constraint file '" + constraint + "' is not a file.");
130 }
131 if (!Files.isReadable(constraint)) {
132 throw new InvalidArgumentException(
133 "The provided external constraint file '" + constraint + "' is not readable.");
134 }
135 }
136 }
137
138 List<String> extraArgs = cmdLine.getArgList();
139 if (extraArgs.size() != 1) {
140 throw new InvalidArgumentException("The source to validate must be provided.");
141 }
142
143 Path source = Paths.get(extraArgs.get(0));
144 if (!Files.exists(source)) {
145 throw new InvalidArgumentException("The provided source file '" + source + "' does not exist.");
146 }
147 if (!Files.isReadable(source)) {
148 throw new InvalidArgumentException("The provided source file '" + source + "' is not readable.");
149 }
150
151 if (cmdLine.hasOption(AS_OPTION)) {
152 try {
153 String toFormatText = cmdLine.getOptionValue(AS_OPTION);
154 Format.valueOf(toFormatText.toUpperCase(Locale.ROOT));
155 } catch (IllegalArgumentException ex) {
156 InvalidArgumentException newEx = new InvalidArgumentException(
157 String.format("Invalid '%s' argument. The format must be one of: %s.",
158 OptionUtils.toArgument(AS_OPTION),
159 Arrays.asList(Format.values()).stream()
160 .map(format -> format.name())
161 .collect(CustomCollectors.joiningWithOxfordComma("and"))));
162 newEx.addSuppressed(ex);
163 throw newEx;
164 }
165 }
166 }
167
168 protected abstract class AbstractValidationCommandExecutor
169 extends AbstractCommandExecutor
170 implements IValidationSchemaProvider {
171
172 public AbstractValidationCommandExecutor(
173 @NonNull CallingContext callingContext,
174 @NonNull CommandLine commandLine) {
175 super(callingContext, commandLine);
176 }
177
178 @NonNull
179 protected abstract IBindingContext getBindingContext(@NonNull Set<IConstraintSet> constraintSets)
180 throws MetaschemaException, IOException;
181
182 @SuppressWarnings("PMD.OnlyOneReturn")
183 @Override
184 public ExitStatus execute() {
185 CommandLine cmdLine = getCommandLine();
186
187 Set<IConstraintSet> constraintSets;
188 if (cmdLine.hasOption(CONSTRAINTS_OPTION)) {
189 ConstraintLoader constraintLoader = new ConstraintLoader();
190 constraintSets = new LinkedHashSet<>();
191 String[] args = cmdLine.getOptionValues(CONSTRAINTS_OPTION);
192 for (String arg : args) {
193 Path constraintPath = Paths.get(arg);
194 assert constraintPath != null;
195 try {
196 constraintSets.add(constraintLoader.load(constraintPath));
197 } catch (IOException | MetaschemaException ex) {
198 return ExitCode.IO_ERROR.exitMessage("Unable to load constraint set '" + arg + "'.").withThrowable(ex);
199 }
200 }
201 } else {
202 constraintSets = CollectionUtil.emptySet();
203 }
204 IBindingContext bindingContext;
205 try {
206 bindingContext = getBindingContext(constraintSets);
207 } catch (IOException | MetaschemaException ex) {
208 return ExitCode.PROCESSING_ERROR
209 .exitMessage("Unable to get binding context. " + ex.getMessage())
210 .withThrowable(ex);
211 }
212
213 IBoundLoader loader = bindingContext.newBoundLoader();
214
215 List<String> extraArgs = cmdLine.getArgList();
216 @SuppressWarnings("null") Path source = resolvePathAgainstCWD(Paths.get(extraArgs.get(0)));
217 assert source != null;
218
219 Format asFormat;
220 if (cmdLine.hasOption(AS_OPTION)) {
221 try {
222 String toFormatText = cmdLine.getOptionValue(AS_OPTION);
223 asFormat = Format.valueOf(toFormatText.toUpperCase(Locale.ROOT));
224 } catch (IllegalArgumentException ex) {
225 return ExitCode.IO_ERROR
226 .exitMessage("Invalid '--as' argument. The format must be one of: "
227 + Arrays.stream(Format.values())
228 .map(format -> format.name())
229 .collect(CustomCollectors.joiningWithOxfordComma("or")))
230 .withThrowable(ex);
231 }
232 } else {
233
234 FormatDetector.Result formatResult;
235 try {
236 formatResult = loader.detectFormat(source);
237 } catch (FileNotFoundException ex) {
238
239 return ExitCode.IO_ERROR.exitMessage("The provided source file '" + source + "' does not exist.");
240 } catch (IOException ex) {
241 return ExitCode.PROCESSING_ERROR.exit().withThrowable(ex);
242 } catch (IllegalArgumentException ex) {
243 return ExitCode.IO_ERROR.exitMessage(
244 "Source file has unrecognizable format. Use '--as' to specify the format. The format must be one of: "
245 + Arrays.stream(Format.values())
246 .map(format -> format.name())
247 .collect(CustomCollectors.joiningWithOxfordComma("or")));
248 }
249 asFormat = formatResult.getFormat();
250 }
251
252 if (LOGGER.isInfoEnabled()) {
253 LOGGER.info("Validating '{}' as {}.", source, asFormat.name());
254 }
255
256 IValidationResult validationResult;
257 try {
258 validationResult = bindingContext.validate(source, asFormat, this);
259 } catch (IOException | SAXException ex) {
260 return ExitCode.PROCESSING_ERROR.exit().withThrowable(ex);
261 }
262
263 if (LOGGER.isInfoEnabled()) {
264 LOGGER.info("Validation identified the following in file '{}'.", source);
265 }
266
267 LoggingValidationHandler.instance().handleValidationResults(validationResult);
268
269 if (validationResult.isPassing() && !cmdLine.hasOption(CLIProcessor.QUIET_OPTION) && LOGGER.isInfoEnabled()) {
270 LOGGER.info("The file '{}' is valid.", source);
271 }
272
273 return (validationResult.isPassing() ? ExitCode.OK : ExitCode.FAIL).exit();
274 }
275
276 }
277 }