1
2
3
4
5
6 package gov.nist.secauto.metaschema.cli.commands;
7
8 import gov.nist.secauto.metaschema.cli.processor.CLIProcessor.CallingContext;
9 import gov.nist.secauto.metaschema.cli.processor.ExitCode;
10 import gov.nist.secauto.metaschema.cli.processor.ExitStatus;
11 import gov.nist.secauto.metaschema.cli.processor.InvalidArgumentException;
12 import gov.nist.secauto.metaschema.cli.processor.OptionUtils;
13 import gov.nist.secauto.metaschema.cli.processor.command.AbstractCommandExecutor;
14 import gov.nist.secauto.metaschema.cli.processor.command.AbstractTerminalCommand;
15 import gov.nist.secauto.metaschema.cli.processor.command.DefaultExtraArgument;
16 import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument;
17 import gov.nist.secauto.metaschema.core.util.CustomCollectors;
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.Format;
22 import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
23
24 import org.apache.commons.cli.CommandLine;
25 import org.apache.commons.cli.Option;
26 import org.apache.logging.log4j.LogManager;
27 import org.apache.logging.log4j.Logger;
28
29 import java.io.FileNotFoundException;
30 import java.io.IOException;
31 import java.io.OutputStreamWriter;
32 import java.io.Writer;
33 import java.net.URI;
34 import java.net.URISyntaxException;
35 import java.nio.charset.StandardCharsets;
36 import java.nio.file.Files;
37 import java.nio.file.Path;
38 import java.nio.file.Paths;
39 import java.nio.file.StandardOpenOption;
40 import java.util.Collection;
41 import java.util.List;
42 import java.util.Locale;
43
44 import edu.umd.cs.findbugs.annotations.NonNull;
45
46 public abstract class AbstractConvertSubcommand
47 extends AbstractTerminalCommand {
48 private static final Logger LOGGER = LogManager.getLogger(AbstractConvertSubcommand.class);
49
50 @NonNull
51 private static final String COMMAND = "convert";
52 @NonNull
53 private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
54 new DefaultExtraArgument("source-file-or-URL", true),
55 new DefaultExtraArgument("destination-file", false)));
56
57 @NonNull
58 private static final Option OVERWRITE_OPTION = ObjectUtils.notNull(
59 Option.builder()
60 .longOpt("overwrite")
61 .desc("overwrite the destination if it exists")
62 .build());
63 @NonNull
64 private static final Option TO_OPTION = ObjectUtils.notNull(
65 Option.builder()
66 .longOpt("to")
67 .required()
68 .hasArg().argName("FORMAT")
69 .desc("convert to format: xml, json, or yaml")
70 .build());
71
72 @Override
73 public String getName() {
74 return COMMAND;
75 }
76
77 @Override
78 public Collection<? extends Option> gatherOptions() {
79 return ObjectUtils.notNull(List.of(
80 OVERWRITE_OPTION,
81 TO_OPTION));
82 }
83
84 @Override
85 public List<ExtraArgument> getExtraArguments() {
86 return EXTRA_ARGUMENTS;
87 }
88
89 @SuppressWarnings("PMD.PreserveStackTrace")
90 @Override
91 public void validateOptions(CallingContext callingContext, CommandLine cmdLine) throws InvalidArgumentException {
92
93 try {
94 String toFormatText = cmdLine.getOptionValue(TO_OPTION);
95 Format.valueOf(toFormatText.toUpperCase(Locale.ROOT));
96 } catch (IllegalArgumentException ex) {
97 InvalidArgumentException newEx = new InvalidArgumentException(
98 String.format("Invalid '%s' argument. The format must be one of: %s.",
99 OptionUtils.toArgument(TO_OPTION),
100 Format.names().stream()
101 .collect(CustomCollectors.joiningWithOxfordComma("and"))));
102 newEx.setOption(TO_OPTION);
103 newEx.addSuppressed(ex);
104 throw newEx;
105 }
106
107 List<String> extraArgs = cmdLine.getArgList();
108 if (extraArgs.isEmpty() || extraArgs.size() > 2) {
109 throw new InvalidArgumentException("Illegal number of arguments.");
110 }
111 }
112
113 protected abstract static class AbstractConversionCommandExecutor
114 extends AbstractCommandExecutor {
115
116
117
118
119
120
121
122
123
124 protected AbstractConversionCommandExecutor(
125 @NonNull CallingContext callingContext,
126 @NonNull CommandLine commandLine) {
127 super(callingContext, commandLine);
128 }
129
130
131
132
133
134
135 @NonNull
136 protected abstract IBindingContext getBindingContext();
137
138 @SuppressWarnings({
139 "PMD.OnlyOneReturn",
140 "PMD.CyclomaticComplexity", "PMD.CognitiveComplexity"
141 })
142 @Override
143 public ExitStatus execute() {
144 CommandLine cmdLine = getCommandLine();
145
146 List<String> extraArgs = cmdLine.getArgList();
147
148 Path destination = null;
149 if (extraArgs.size() > 1) {
150 destination = Paths.get(extraArgs.get(1)).toAbsolutePath();
151 }
152
153 if (destination != null) {
154 if (Files.exists(destination)) {
155 if (!cmdLine.hasOption(OVERWRITE_OPTION)) {
156 return ExitCode.INVALID_ARGUMENTS.exitMessage(
157 String.format("The provided destination '%s' already exists and the '%s' option was not provided.",
158 destination,
159 OptionUtils.toArgument(OVERWRITE_OPTION)));
160 }
161 if (!Files.isWritable(destination)) {
162 return ExitCode.IO_ERROR.exitMessage(
163 "The provided destination '" + destination + "' is not writable.");
164 }
165 } else {
166 Path parent = destination.getParent();
167 if (parent != null) {
168 try {
169 Files.createDirectories(parent);
170 } catch (IOException ex) {
171 return ExitCode.INVALID_TARGET.exit().withThrowable(ex);
172 }
173 }
174 }
175 }
176
177 String sourceName = ObjectUtils.notNull(extraArgs.get(0));
178 URI cwd = ObjectUtils.notNull(Paths.get("").toAbsolutePath().toUri());
179
180 URI source;
181 try {
182 source = UriUtils.toUri(sourceName, cwd);
183 } catch (URISyntaxException ex) {
184 return ExitCode.IO_ERROR.exitMessage("Cannot load source '%s' as it is not a valid file or URI.")
185 .withThrowable(ex);
186 }
187 assert source != null;
188
189 String toFormatText = cmdLine.getOptionValue(TO_OPTION);
190 Format toFormat = Format.valueOf(toFormatText.toUpperCase(Locale.ROOT));
191
192 IBindingContext bindingContext = getBindingContext();
193 try {
194 IBoundLoader loader = bindingContext.newBoundLoader();
195 if (LOGGER.isInfoEnabled()) {
196 LOGGER.info("Converting '{}'.", source);
197 }
198
199 if (destination == null) {
200
201 OutputStreamWriter writer = new OutputStreamWriter(System.out, StandardCharsets.UTF_8);
202 handleConversion(source, toFormat, writer, loader);
203 } else {
204 try (Writer writer = Files.newBufferedWriter(
205 destination,
206 StandardCharsets.UTF_8,
207 StandardOpenOption.CREATE,
208 StandardOpenOption.WRITE,
209 StandardOpenOption.TRUNCATE_EXISTING)) {
210 assert writer != null;
211 handleConversion(source, toFormat, writer, loader);
212 }
213 }
214 } catch (IOException | IllegalArgumentException ex) {
215 return ExitCode.PROCESSING_ERROR.exit().withThrowable(ex);
216 }
217 if (destination != null && LOGGER.isInfoEnabled()) {
218 LOGGER.info("Generated {} file: {}", toFormat.toString(), destination);
219 }
220 return ExitCode.OK.exit();
221 }
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239 protected abstract void handleConversion(
240 @NonNull URI source,
241 @NonNull Format toFormat,
242 @NonNull Writer writer,
243 @NonNull IBoundLoader loader) throws FileNotFoundException, IOException;
244 }
245 }