1
2
3
4
5
6 package gov.nist.secauto.oscal.tools.cli.core.commands;
7
8 import gov.nist.secauto.metaschema.cli.commands.MetaschemaCommands;
9 import gov.nist.secauto.metaschema.cli.processor.CLIProcessor.CallingContext;
10 import gov.nist.secauto.metaschema.cli.processor.ExitCode;
11 import gov.nist.secauto.metaschema.cli.processor.command.AbstractTerminalCommand;
12 import gov.nist.secauto.metaschema.cli.processor.command.CommandExecutionException;
13 import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument;
14 import gov.nist.secauto.metaschema.cli.processor.command.ICommandExecutor;
15 import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
16 import gov.nist.secauto.metaschema.core.metapath.item.node.IDocumentNodeItem;
17 import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem;
18 import gov.nist.secauto.metaschema.core.util.ObjectUtils;
19 import gov.nist.secauto.metaschema.core.util.UriUtils;
20 import gov.nist.secauto.metaschema.databind.IBindingContext;
21 import gov.nist.secauto.metaschema.databind.io.DeserializationFeature;
22 import gov.nist.secauto.metaschema.databind.io.Format;
23 import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
24 import gov.nist.secauto.metaschema.databind.io.ISerializer;
25 import gov.nist.secauto.oscal.lib.OscalBindingContext;
26 import gov.nist.secauto.oscal.lib.model.Catalog;
27 import gov.nist.secauto.oscal.lib.model.Profile;
28 import gov.nist.secauto.oscal.lib.profile.resolver.ProfileResolutionException;
29 import gov.nist.secauto.oscal.lib.profile.resolver.ProfileResolver;
30 import gov.nist.secauto.oscal.tools.cli.core.utils.PrettyPrinter;
31
32 import org.apache.commons.cli.CommandLine;
33 import org.apache.commons.cli.Option;
34
35 import java.io.IOException;
36 import java.io.PrintStream;
37 import java.net.URI;
38 import java.net.URISyntaxException;
39 import java.nio.file.Path;
40 import java.util.Collection;
41 import java.util.List;
42
43 import edu.umd.cs.findbugs.annotations.NonNull;
44
45
46
47
48 public abstract class AbstractResolveCommand
49 extends AbstractTerminalCommand {
50 @NonNull
51 private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
52 ExtraArgument.newInstance("URI to resolve", true),
53 ExtraArgument.newInstance("destination file", false)));
54 private static final Option RELATIVE_TO = Option.builder()
55 .longOpt("relative-to")
56 .desc("Generate URI references relative to this resource")
57 .hasArg()
58 .build();
59 private static final Option PRETTY_PRINT_OPTION = Option.builder()
60 .longOpt("pretty-print")
61 .desc("Enable pretty-printing of the output for better readability")
62 .build();
63
64 @NonNull
65 private static final List<Option> OPTIONS = ObjectUtils.notNull(
66 List.of(
67 MetaschemaCommands.AS_FORMAT_OPTION,
68 MetaschemaCommands.TO_OPTION,
69 MetaschemaCommands.OVERWRITE_OPTION,
70 RELATIVE_TO,
71 PRETTY_PRINT_OPTION));
72
73 @Override
74 public String getDescription() {
75 return "Resolve the specified OSCAL Profile";
76 }
77
78 @Override
79 public Collection<? extends Option> gatherOptions() {
80 return OPTIONS;
81 }
82
83 @Override
84 public List<ExtraArgument> getExtraArguments() {
85 return EXTRA_ARGUMENTS;
86 }
87
88 @SuppressWarnings({
89 "PMD.CyclomaticComplexity", "PMD.CognitiveComplexity",
90 "PMD.PreserveStackTrace"
91 })
92
93 @Override
94 public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) {
95 return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand);
96 }
97
98
99
100
101
102
103
104
105
106
107
108
109 @SuppressWarnings({
110 "PMD.OnlyOneReturn",
111 "PMD.CyclomaticComplexity"
112 })
113 protected void executeCommand(
114 @NonNull CallingContext callingContext,
115 @NonNull CommandLine cmdLine) throws CommandExecutionException {
116 List<String> extraArgs = cmdLine.getArgList();
117
118 URI source = MetaschemaCommands.handleSource(
119 ObjectUtils.requireNonNull(extraArgs.get(0)),
120 ObjectUtils.notNull(getCurrentWorkingDirectory().toUri()));
121
122 IBindingContext bindingContext = OscalBindingContext.instance();
123 IBoundLoader loader = bindingContext.newBoundLoader();
124 loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_CONSTRAINTS);
125
126
127 Format asFormat = MetaschemaCommands.determineSourceFormat(
128 cmdLine,
129 MetaschemaCommands.AS_FORMAT_OPTION,
130 loader,
131 source);
132
133 IDocumentNodeItem document;
134 try {
135 document = loader.loadAsNodeItem(asFormat, source);
136 } catch (IOException ex) {
137 throw new CommandExecutionException(
138 ExitCode.IO_ERROR,
139 String.format("Unable to load content '%s'. %s",
140 source,
141 ex.getMessage()),
142 ex);
143 }
144
145 Object object = document.getValue();
146 if (object == null) {
147 throw new CommandExecutionException(
148 ExitCode.INVALID_ARGUMENTS,
149 String.format("The source document '%s' contained no data.", source));
150 }
151
152 if (object instanceof Catalog) {
153
154 throw new CommandExecutionException(
155 ExitCode.OK,
156 String.format("The source '%s' is already a catalog.", source));
157 }
158
159 if (!(object instanceof Profile)) {
160
161 throw new CommandExecutionException(
162 ExitCode.INVALID_ARGUMENTS,
163 String.format("The source '%s' is not a profile.", source));
164 }
165
166 Path destination = null;
167 if (extraArgs.size() > 1) {
168 destination = MetaschemaCommands.handleDestination(ObjectUtils.requireNonNull(extraArgs.get(1)), cmdLine);
169 }
170
171 URI relativeTo;
172 if (cmdLine.hasOption(RELATIVE_TO)) {
173 relativeTo = getCurrentWorkingDirectory().toUri().resolve(cmdLine.getOptionValue(RELATIVE_TO));
174 } else {
175 relativeTo = document.getDocumentUri();
176 }
177
178
179 DynamicContext dynamicContext = new DynamicContext(document.getStaticContext());
180 dynamicContext.setDocumentLoader(loader);
181 ProfileResolver resolver = new ProfileResolver(
182 dynamicContext,
183 (uri, src) -> {
184 try {
185 return UriUtils.relativize(relativeTo, src.resolve(uri), true);
186 } catch (URISyntaxException ex) {
187 throw new IllegalArgumentException(ex);
188 }
189 });
190
191 IDocumentNodeItem resolvedProfile;
192 try {
193 resolvedProfile = resolver.resolve(document);
194 } catch (IOException | ProfileResolutionException ex) {
195 throw new CommandExecutionException(
196 ExitCode.PROCESSING_ERROR,
197 String.format("Cmd: Unable to resolve profile '%s'. %s", document.getDocumentUri(), ex.getMessage()),
198 ex);
199 }
200
201
202
203
204
205
206 Format toFormat = MetaschemaCommands.getFormat(cmdLine, MetaschemaCommands.TO_OPTION);
207 boolean prettyPrint = cmdLine.hasOption(PRETTY_PRINT_OPTION);
208 ISerializer<Catalog> serializer = bindingContext.newSerializer(toFormat, Catalog.class);
209 try {
210 if (destination == null) {
211 @SuppressWarnings({ "resource", "PMD.CloseResource" })
212 PrintStream stdOut = ObjectUtils.notNull(System.out);
213 serializer.serialize((Catalog) INodeItem.toValue(resolvedProfile), stdOut);
214 } else {
215 serializer.serialize((Catalog) INodeItem.toValue(resolvedProfile), destination);
216 if (prettyPrint) {
217 prettyPrintOutput(destination, toFormat);
218 }
219 }
220 } catch (IOException ex) {
221 throw new CommandExecutionException(ExitCode.IO_ERROR, ex);
222 }
223 }
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239 @SuppressWarnings("PMD.PreserveStackTrace")
240 private void prettyPrintOutput(@NonNull Path destination, @NonNull Format toFormat)
241 throws CommandExecutionException {
242 try {
243 switch (toFormat) {
244 case JSON:
245 PrettyPrinter.prettyPrintJson(destination.toFile());
246 break;
247 case YAML:
248 PrettyPrinter.prettyPrintYaml(destination.toFile());
249 break;
250 case XML:
251 PrettyPrinter.prettyPrintXml(destination.toFile());
252 break;
253 default:
254
255 break;
256 }
257 } catch (Exception ex) {
258 throw new CommandExecutionException(
259 ExitCode.PROCESSING_ERROR,
260 String.format("Pretty-printing failed: %s", ex.getMessage()),
261 ex);
262 }
263 }
264 }