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