001/* 002 * SPDX-FileCopyrightText: none 003 * SPDX-License-Identifier: CC0-1.0 004 */ 005 006package dev.metaschema.oscal.tools.cli.core.commands; 007 008import org.apache.commons.cli.CommandLine; 009import org.apache.commons.cli.Option; 010 011import java.io.IOException; 012import java.io.PrintStream; 013import java.net.URI; 014import java.net.URISyntaxException; 015import java.nio.file.Path; 016import java.util.Collection; 017import java.util.List; 018 019import dev.metaschema.cli.commands.MetaschemaCommands; 020import dev.metaschema.cli.processor.CallingContext; 021import dev.metaschema.cli.processor.ExitCode; 022import dev.metaschema.cli.processor.command.AbstractTerminalCommand; 023import dev.metaschema.cli.processor.command.CommandExecutionException; 024import dev.metaschema.cli.processor.command.ExtraArgument; 025import dev.metaschema.cli.processor.command.ICommandExecutor; 026import dev.metaschema.core.metapath.DynamicContext; 027import dev.metaschema.core.metapath.item.node.IDocumentNodeItem; 028import dev.metaschema.core.metapath.item.node.INodeItem; 029import dev.metaschema.core.util.ObjectUtils; 030import dev.metaschema.core.util.UriUtils; 031import dev.metaschema.databind.IBindingContext; 032import dev.metaschema.databind.io.DeserializationFeature; 033import dev.metaschema.databind.io.Format; 034import dev.metaschema.databind.io.IBoundLoader; 035import dev.metaschema.databind.io.ISerializer; 036import dev.metaschema.oscal.lib.OscalBindingContext; 037import dev.metaschema.oscal.lib.model.Catalog; 038import dev.metaschema.oscal.lib.model.Profile; 039import dev.metaschema.oscal.lib.profile.resolver.ProfileResolutionException; 040import dev.metaschema.oscal.lib.profile.resolver.ProfileResolver; 041import dev.metaschema.oscal.tools.cli.core.utils.PrettyPrinter; 042import edu.umd.cs.findbugs.annotations.NonNull; 043 044/** 045 * A command implementation supporting the resolution of an OSCAL profile. 046 */ 047public abstract class AbstractResolveCommand 048 extends AbstractTerminalCommand { 049 @NonNull 050 private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of( 051 ExtraArgument.newInstance("URI to resolve", true), 052 ExtraArgument.newInstance("destination file", false))); 053 private static final Option RELATIVE_TO = Option.builder() 054 .longOpt("relative-to") 055 .desc("Generate URI references relative to this resource") 056 .hasArg() 057 .build(); 058 private static final Option PRETTY_PRINT_OPTION = Option.builder() 059 .longOpt("pretty-print") 060 .desc("Enable pretty-printing of the output for better readability") 061 .build(); 062 063 @NonNull 064 private static final List<Option> OPTIONS = ObjectUtils.notNull( 065 List.of( 066 MetaschemaCommands.AS_FORMAT_OPTION, 067 MetaschemaCommands.TO_OPTION, 068 MetaschemaCommands.OVERWRITE_OPTION, 069 RELATIVE_TO, 070 PRETTY_PRINT_OPTION)); 071 072 @Override 073 public String getDescription() { 074 return "Resolve the specified OSCAL Profile"; 075 } 076 077 @Override 078 public Collection<? extends Option> gatherOptions() { 079 return OPTIONS; 080 } 081 082 @Override 083 public List<ExtraArgument> getExtraArguments() { 084 return EXTRA_ARGUMENTS; 085 } 086 087 @SuppressWarnings({ 088 "PMD.CyclomaticComplexity", "PMD.CognitiveComplexity", // reasonable 089 "PMD.PreserveStackTrace" // intended 090 }) 091 092 @Override 093 public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) { 094 return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand); 095 } 096 097 /** 098 * Process the command line arguments and execute the profile resolution 099 * operation. 100 * 101 * @param callingContext 102 * the context information for the execution 103 * @param cmdLine 104 * the parsed command line details 105 * @throws CommandExecutionException 106 * if an error occurred while determining the source format 107 */ 108 @SuppressWarnings({ 109 "PMD.OnlyOneReturn", // readability 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 // attempt to determine the format 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 // this is a catalog 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 // this is something else 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 // this is a profile 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 // DefaultConstraintValidator validator = new 201 // DefaultConstraintValidator(dynamicContext); 202 // ((IBoundXdmNodeItem)resolvedProfile).validate(validator); 203 // validator.finalizeValidation(); 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 * Pretty-print the output file based on the specified format. 226 * <p> 227 * This feature was originally contributed by Mahesh Kumar Gaddam (ermahesh) in 228 * <a href="https://github.com/usnistgov/oscal-cli/pull/295">PR #295</a>. 229 * </p> 230 * 231 * @param destination 232 * the path to the output file 233 * @param toFormat 234 * the format of the output file 235 * @throws CommandExecutionException 236 * if pretty-printing fails 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 // do nothing for unknown formats 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}