1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.cli.commands.metapath;
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.ExitStatus;
12  import gov.nist.secauto.metaschema.cli.processor.InvalidArgumentException;
13  import gov.nist.secauto.metaschema.cli.processor.command.AbstractTerminalCommand;
14  import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument;
15  import gov.nist.secauto.metaschema.cli.processor.command.ICommandExecutor;
16  import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
17  import gov.nist.secauto.metaschema.core.metapath.ISequence;
18  import gov.nist.secauto.metaschema.core.metapath.MetapathExpression;
19  import gov.nist.secauto.metaschema.core.metapath.StaticContext;
20  import gov.nist.secauto.metaschema.core.metapath.item.DefaultItemWriter;
21  import gov.nist.secauto.metaschema.core.metapath.item.IItemWriter;
22  import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem;
23  import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItemFactory;
24  import gov.nist.secauto.metaschema.core.model.IModule;
25  import gov.nist.secauto.metaschema.core.model.MetaschemaException;
26  import gov.nist.secauto.metaschema.core.util.CollectionUtil;
27  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
28  import gov.nist.secauto.metaschema.core.util.UriUtils;
29  import gov.nist.secauto.metaschema.databind.DefaultBindingContext;
30  import gov.nist.secauto.metaschema.databind.IBindingContext;
31  import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
32  
33  import org.apache.commons.cli.CommandLine;
34  import org.apache.commons.cli.Option;
35  import org.apache.logging.log4j.LogManager;
36  import org.apache.logging.log4j.Logger;
37  
38  import java.io.IOException;
39  import java.io.PrintWriter;
40  import java.io.StringWriter;
41  import java.io.Writer;
42  import java.net.URI;
43  import java.net.URISyntaxException;
44  import java.nio.file.Files;
45  import java.nio.file.Path;
46  import java.nio.file.Paths;
47  import java.util.Collection;
48  import java.util.List;
49  
50  import edu.umd.cs.findbugs.annotations.NonNull;
51  
52  public class EvaluateMetapathCommand
53      extends AbstractTerminalCommand {
54    private static final Logger LOGGER = LogManager.getLogger(EvaluateMetapathCommand.class);
55  
56    @NonNull
57    private static final String COMMAND = "eval";
58    @NonNull
59    private static final Option EXPRESSION_OPTION = ObjectUtils.notNull(
60        Option.builder("e")
61            .longOpt("expression")
62            .required()
63            .hasArg()
64            .argName("EXPRESSION")
65            .desc("Metapath expression to execute")
66            .build());
67    @NonNull
68    public static final Option CONTENT_OPTION = ObjectUtils.notNull(
69        Option.builder("i")
70            .hasArg()
71            .argName("FILE_OR_URL")
72            .desc("Metaschema content instance resource")
73            .build());
74  
75    @NonNull
76    public static final Option METASCHEMA_OPTION = ObjectUtils.notNull(
77        Option.builder("m")
78            .hasArg()
79            .argName("FILE_OR_URL")
80            .desc("metaschema resource")
81            .build());
82  
83    @Override
84    public String getName() {
85      return COMMAND;
86    }
87  
88    @Override
89    public String getDescription() {
90      return "Execute a Metapath expression against a document";
91    }
92  
93    @SuppressWarnings("null")
94    @Override
95    public Collection<? extends Option> gatherOptions() {
96      return List.of(
97          METASCHEMA_OPTION,
98          CONTENT_OPTION,
99          EXPRESSION_OPTION);
100   }
101 
102   @Override
103   public List<ExtraArgument> getExtraArguments() {
104     return CollectionUtil.emptyList();
105   }
106 
107   @Override
108   public void validateOptions(CallingContext callingContext, CommandLine cmdLine) throws InvalidArgumentException {
109     List<String> extraArgs = cmdLine.getArgList();
110     if (!extraArgs.isEmpty()) {
111       throw new InvalidArgumentException("Illegal number of extra arguments.");
112     }
113   }
114 
115   @Override
116   public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) {
117     return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand);
118   }
119 
120   @SuppressWarnings({
121       "PMD.OnlyOneReturn", // readability
122   })
123 
124   protected ExitStatus executeCommand(
125       @NonNull CallingContext callingContext,
126       @NonNull CommandLine cmdLine) {
127     URI cwd = ObjectUtils.notNull(Paths.get("").toAbsolutePath().toUri());
128 
129     IModule module;
130     INodeItem item;
131     if (cmdLine.hasOption(METASCHEMA_OPTION)) {
132       try {
133         String moduleName
134             = ObjectUtils.requireNonNull(cmdLine.getOptionValue(METASCHEMA_OPTION));
135         URI moduleUri = UriUtils.toUri(moduleName, cwd);
136         module = MetaschemaCommands.handleModule(moduleUri, CollectionUtil.emptyList());
137       } catch (URISyntaxException ex) {
138         return ExitCode.INVALID_ARGUMENTS
139             .exitMessage(
140                 String.format("Cannot load module as '%s' is not a valid file or URL.", ex.getInput()))
141             .withThrowable(ex);
142       } catch (IOException | MetaschemaException ex) {
143         return ExitCode.PROCESSING_ERROR.exit().withThrowable(ex);
144       }
145 
146       // determine if the query is evaluated against the module or the instance
147       if (cmdLine.hasOption(CONTENT_OPTION)) {
148         // load the content
149         IBindingContext bindingContext = new DefaultBindingContext();
150 
151         try {
152           Path compilePath = Files.createTempDirectory("validation-");
153           compilePath.toFile().deleteOnExit();
154 
155           bindingContext.registerModule(module, compilePath);
156         } catch (IOException ex) {
157           return ExitCode.PROCESSING_ERROR
158               .exitMessage("Unable to get binding context. " + ex.getMessage())
159               .withThrowable(ex);
160         }
161 
162         IBoundLoader loader = bindingContext.newBoundLoader();
163 
164         URI contentResource;
165         try {
166           contentResource = MetaschemaCommands.handleResource(cmdLine.getOptionValue(CONTENT_OPTION), cwd);
167         } catch (IOException ex) {
168           return ExitCode.INVALID_ARGUMENTS
169               .exitMessage("Unable to resolve content location. " + ex.getMessage())
170               .withThrowable(ex);
171         }
172 
173         try {
174           item = loader.loadAsNodeItem(contentResource);
175         } catch (IOException ex) {
176           return ExitCode.INVALID_ARGUMENTS
177               .exitMessage("Unable to resolve content location. " + ex.getMessage())
178               .withThrowable(ex);
179         }
180       } else {
181         item = INodeItemFactory.instance().newModuleNodeItem(module);
182       }
183     } else if (cmdLine.hasOption(CONTENT_OPTION)) {
184       return ExitCode.INVALID_ARGUMENTS.exitMessage(
185           String.format("Must use '%s' to specify the Metaschema module.", CONTENT_OPTION.getArgName()));
186     } else {
187       module = null;
188       item = null;
189     }
190 
191     String expression = cmdLine.getOptionValue(EXPRESSION_OPTION);
192 
193     StaticContext.Builder builder = StaticContext.builder();
194     if (module != null) {
195       builder.defaultModelNamespace(module.getXmlNamespace());
196     }
197     StaticContext staticContext = builder.build();
198 
199     try {
200       // Parse and compile the Metapath expression
201       MetapathExpression compiledMetapath = MetapathExpression.compile(expression, staticContext);
202       ISequence<?> sequence = compiledMetapath.evaluate(item, new DynamicContext(staticContext));
203 
204       Writer stringWriter = new StringWriter();
205       try (PrintWriter writer = new PrintWriter(stringWriter)) {
206         IItemWriter itemWriter = new DefaultItemWriter(writer);
207         itemWriter.writeSequence(sequence);
208       }
209 
210       // Print the result
211       if (LOGGER.isInfoEnabled()) {
212         LOGGER.info(stringWriter.toString());
213       }
214 
215       return ExitCode.OK.exit();
216     } catch (Exception ex) {
217       return ExitCode.PROCESSING_ERROR.exit().withThrowable(ex);
218     }
219   }
220 }