1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.cli.commands.metapath;
7   
8   import org.apache.commons.cli.CommandLine;
9   import org.apache.commons.cli.Option;
10  import org.apache.logging.log4j.LogManager;
11  import org.apache.logging.log4j.Logger;
12  
13  import java.io.IOException;
14  import java.io.PrintWriter;
15  import java.io.StringWriter;
16  import java.io.Writer;
17  import java.net.URI;
18  import java.net.URISyntaxException;
19  import java.util.Collection;
20  import java.util.List;
21  
22  import dev.metaschema.cli.commands.MetaschemaCommands;
23  import dev.metaschema.cli.processor.CallingContext;
24  import dev.metaschema.cli.processor.ExitCode;
25  import dev.metaschema.cli.processor.command.AbstractTerminalCommand;
26  import dev.metaschema.cli.processor.command.CommandExecutionException;
27  import dev.metaschema.cli.processor.command.ExtraArgument;
28  import dev.metaschema.cli.processor.command.ICommandExecutor;
29  import dev.metaschema.core.metapath.DynamicContext;
30  import dev.metaschema.core.metapath.IMetapathExpression;
31  import dev.metaschema.core.metapath.StaticContext;
32  import dev.metaschema.core.metapath.item.DefaultItemWriter;
33  import dev.metaschema.core.metapath.item.IItemWriter;
34  import dev.metaschema.core.metapath.item.ISequence;
35  import dev.metaschema.core.metapath.item.node.INodeItem;
36  import dev.metaschema.core.metapath.item.node.INodeItemFactory;
37  import dev.metaschema.core.model.IModule;
38  import dev.metaschema.core.model.MetaschemaException;
39  import dev.metaschema.core.util.CollectionUtil;
40  import dev.metaschema.core.util.ObjectUtils;
41  import dev.metaschema.databind.IBindingContext;
42  import dev.metaschema.databind.io.IBoundLoader;
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,
53   * <li><b>module-only:</b> against the Metaschema module itself if no content
54   * instance is provided, or
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.
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            .get());
75    @NonNull
76    private static final Option CONTENT_OPTION = ObjectUtils.notNull(
77        Option.builder("i")
78            .hasArg()
79            .argName("FILE_OR_URL")
80            .type(URI.class)
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   @SuppressFBWarnings(value = "REC_CATCH_EXCEPTION",
114       justification = "Catching generic exception for CLI error handling")
115   private void executeCommand(
116       @SuppressWarnings("unused") @NonNull CallingContext callingContext,
117       @NonNull CommandLine cmdLine) throws CommandExecutionException {
118 
119     IModule module = null;
120     INodeItem item = null;
121     IBoundLoader loader = null;
122     if (cmdLine.hasOption(MetaschemaCommands.METASCHEMA_OPTIONAL_OPTION)) {
123       IBindingContext bindingContext = MetaschemaCommands.newBindingContextWithDynamicCompilation();
124 
125       // Use permissive loader since eval is not a validation command
126       loader = bindingContext.newPermissiveBoundLoader();
127 
128       try {
129         module = bindingContext.registerModule(MetaschemaCommands.loadModule(
130             cmdLine,
131             MetaschemaCommands.METASCHEMA_OPTIONAL_OPTION,
132             ObjectUtils.notNull(getCurrentWorkingDirectory().toUri()),
133             bindingContext));
134       } catch (MetaschemaException ex) {
135         throw new CommandExecutionException(ExitCode.PROCESSING_ERROR, ex);
136       }
137 
138       // determine if the query is evaluated against the module or the instance
139       if (cmdLine.hasOption(CONTENT_OPTION)) {
140         // load the content
141         String contentLocation = ObjectUtils.requireNonNull(cmdLine.getOptionValue(CONTENT_OPTION));
142         URI contentResource;
143         try {
144           contentResource = MetaschemaCommands.getResourceUri(
145               contentLocation,
146               ObjectUtils.notNull(getCurrentWorkingDirectory().toUri()));
147         } catch (URISyntaxException ex) {
148           throw new CommandExecutionException(
149               ExitCode.INVALID_ARGUMENTS,
150               String.format("Unable to load content '%s'. %s",
151                   contentLocation,
152                   ex.getMessage()),
153               ex);
154         }
155 
156         try {
157           item = loader.loadAsNodeItem(contentResource);
158         } catch (IOException ex) {
159           throw new CommandExecutionException(
160               ExitCode.INVALID_ARGUMENTS,
161               String.format("Unable to load content '%s'. %s",
162                   contentLocation,
163                   ex.getMessage()),
164               ex);
165         }
166       } else {
167         // evaluate against the module
168         item = INodeItemFactory.instance().newModuleNodeItem(module);
169       }
170     } else if (cmdLine.hasOption(CONTENT_OPTION)) {
171       // content provided, but no module; require module
172       throw new CommandExecutionException(
173           ExitCode.INVALID_ARGUMENTS,
174           String.format("Must use '%s' to specify the Metaschema module.",
175               CONTENT_OPTION.getArgName()));
176     }
177 
178     // now setup to evaluate the metapath
179     StaticContext.Builder builder = StaticContext.builder();
180     if (module != null) {
181       builder.defaultModelNamespace(module.getXmlNamespace());
182     }
183     StaticContext staticContext = builder.build();
184 
185     String expression = cmdLine.getOptionValue(EXPRESSION_OPTION);
186     if (expression == null) {
187       throw new CommandExecutionException(
188           ExitCode.INVALID_ARGUMENTS,
189           String.format("Must use '%s' to specify the Metapath expression.", EXPRESSION_OPTION.getArgName()));
190     }
191 
192     // Setup dynamic context with document loader for doc() function support
193     DynamicContext dynamicContext = new DynamicContext(staticContext);
194     if (loader != null) {
195       dynamicContext.setDocumentLoader(loader);
196     }
197 
198     try {
199       // Parse and compile the Metapath expression
200       ISequence<?> sequence = IMetapathExpression.compile(expression, staticContext)
201           .evaluate(item, dynamicContext);
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 }