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