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.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.ISequence;
17  import gov.nist.secauto.metaschema.core.metapath.MetapathExpression;
18  import gov.nist.secauto.metaschema.core.metapath.StaticContext;
19  import gov.nist.secauto.metaschema.core.metapath.item.DefaultItemWriter;
20  import gov.nist.secauto.metaschema.core.metapath.item.IItemWriter;
21  import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem;
22  import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItemFactory;
23  import gov.nist.secauto.metaschema.core.model.IModule;
24  import gov.nist.secauto.metaschema.core.util.CollectionUtil;
25  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
26  import gov.nist.secauto.metaschema.databind.IBindingContext;
27  import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
28  
29  import org.apache.commons.cli.CommandLine;
30  import org.apache.commons.cli.Option;
31  import org.apache.logging.log4j.LogManager;
32  import org.apache.logging.log4j.Logger;
33  
34  import java.io.IOException;
35  import java.io.PrintWriter;
36  import java.io.StringWriter;
37  import java.io.Writer;
38  import java.net.URI;
39  import java.net.URISyntaxException;
40  import java.util.Collection;
41  import java.util.List;
42  
43  import edu.umd.cs.findbugs.annotations.NonNull;
44  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
45  
46  /**
47   * This command implementation executes a Metapath query.
48   * <p>
49   * The query is executed using one of the following configurations:
50   * <ol>
51   * <li><b>module and content:</b> on a content instance parsed using a provided
52   * Metaschema module,</li>
53   * <li><b>module-only:</b> against the Metaschema module itself if no content
54   * instance is provided, or</li>
55   * <li><b>without content or module:</b> if both a module and content are
56   * omitted then the execution will be limited to operations that do not act on
57   * content.</li>
58   * </ol>
59   */
60  class EvaluateMetapathCommand
61      extends AbstractTerminalCommand {
62    private static final Logger LOGGER = LogManager.getLogger(EvaluateMetapathCommand.class);
63  
64    @NonNull
65    private static final String COMMAND = "eval";
66    @NonNull
67    private static final Option EXPRESSION_OPTION = ObjectUtils.notNull(
68        Option.builder("e")
69            .longOpt("expression")
70            .required()
71            .hasArg()
72            .argName("EXPRESSION")
73            .desc("Metapath expression to execute")
74            .build());
75    @NonNull
76    private static final Option CONTENT_OPTION = ObjectUtils.notNull(
77        Option.builder("i")
78            .hasArg()
79            .argName("FILE_OR_URL")
80            .desc("Metaschema content instance 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          MetaschemaCommands.METASCHEMA_OPTIONAL_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 ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) {
109     return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand);
110   }
111 
112   @SuppressWarnings({
113       "PMD.OnlyOneReturn", // readability
114       "PMD.AvoidCatchingGenericException",
115       "PMD.NPathComplexity",
116       "PMD.CognitiveComplexity",
117       "PMD.CyclomaticComplexity"
118   })
119   @SuppressFBWarnings(value = "REC_CATCH_EXCEPTION",
120       justification = "Catching generic exception for CLI error handling")
121   private void executeCommand(
122       @SuppressWarnings("unused") @NonNull CallingContext callingContext,
123       @NonNull CommandLine cmdLine) throws CommandExecutionException {
124 
125     IModule module = null;
126     INodeItem item = null;
127     if (cmdLine.hasOption(MetaschemaCommands.METASCHEMA_OPTIONAL_OPTION)) {
128       IBindingContext bindingContext = MetaschemaCommands.newBindingContextWithDynamicCompilation();
129 
130       module = bindingContext.registerModule(MetaschemaCommands.loadModule(
131           cmdLine,
132           MetaschemaCommands.METASCHEMA_OPTIONAL_OPTION,
133           ObjectUtils.notNull(getCurrentWorkingDirectory().toUri()),
134           bindingContext));
135 
136       // determine if the query is evaluated against the module or the instance
137       if (cmdLine.hasOption(CONTENT_OPTION)) {
138         // load the content
139 
140         IBoundLoader loader = bindingContext.newBoundLoader();
141 
142         String contentLocation = ObjectUtils.requireNonNull(cmdLine.getOptionValue(CONTENT_OPTION));
143         URI contentResource;
144         try {
145           contentResource = MetaschemaCommands.getResourceUri(
146               contentLocation,
147               ObjectUtils.notNull(getCurrentWorkingDirectory().toUri()));
148         } catch (URISyntaxException ex) {
149           throw new CommandExecutionException(
150               ExitCode.INVALID_ARGUMENTS,
151               String.format("Unable to load content '%s'. %s",
152                   contentLocation,
153                   ex.getMessage()),
154               ex);
155         }
156 
157         try {
158           item = loader.loadAsNodeItem(contentResource);
159         } catch (IOException ex) {
160           throw new CommandExecutionException(
161               ExitCode.INVALID_ARGUMENTS,
162               String.format("Unable to load content '%s'. %s",
163                   contentLocation,
164                   ex.getMessage()),
165               ex);
166         }
167       } else {
168         // evaluate against the module
169         item = INodeItemFactory.instance().newModuleNodeItem(module);
170       }
171     } else if (cmdLine.hasOption(CONTENT_OPTION)) {
172       // content provided, but no module; require module
173       throw new CommandExecutionException(
174           ExitCode.INVALID_ARGUMENTS,
175           String.format("Must use '%s' to specify the Metaschema module.",
176               CONTENT_OPTION.getArgName()));
177     }
178 
179     // now setup to evaluate the metapath
180     StaticContext.Builder builder = StaticContext.builder();
181     if (module != null) {
182       builder.defaultModelNamespace(module.getXmlNamespace());
183     }
184     StaticContext staticContext = builder.build();
185 
186     String expression = cmdLine.getOptionValue(EXPRESSION_OPTION);
187     if (expression == null) {
188       throw new CommandExecutionException(
189           ExitCode.INVALID_ARGUMENTS,
190           String.format("Must use '%s' to specify the Metapath expression.", EXPRESSION_OPTION.getArgName()));
191     }
192 
193     try {
194       // Parse and compile the Metapath expression
195       MetapathExpression compiledMetapath = MetapathExpression.compile(expression, staticContext);
196       ISequence<?> sequence = compiledMetapath.evaluate(item, new DynamicContext(staticContext));
197 
198       // handle the metapath results
199       try (Writer stringWriter = new StringWriter()) {
200         try (PrintWriter writer = new PrintWriter(stringWriter)) {
201           try (IItemWriter itemWriter = new DefaultItemWriter(writer)) {
202             itemWriter.writeSequence(sequence);
203           } catch (IOException ex) {
204             throw new CommandExecutionException(ExitCode.IO_ERROR, ex);
205           } catch (Exception ex) {
206             throw new CommandExecutionException(ExitCode.RUNTIME_ERROR, ex);
207           }
208         }
209 
210         // Print the result
211         if (LOGGER.isInfoEnabled()) {
212           LOGGER.info(stringWriter.toString());
213         }
214       } catch (IOException ex) {
215         throw new CommandExecutionException(ExitCode.IO_ERROR, ex);
216       }
217     } catch (RuntimeException ex) {
218       throw new CommandExecutionException(ExitCode.PROCESSING_ERROR, ex);
219     }
220   }
221 }