001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package gov.nist.secauto.metaschema.cli.commands.metapath;
007
008import gov.nist.secauto.metaschema.cli.commands.MetaschemaCommands;
009import gov.nist.secauto.metaschema.cli.processor.CLIProcessor.CallingContext;
010import gov.nist.secauto.metaschema.cli.processor.ExitCode;
011import gov.nist.secauto.metaschema.cli.processor.ExitStatus;
012import gov.nist.secauto.metaschema.cli.processor.InvalidArgumentException;
013import gov.nist.secauto.metaschema.cli.processor.command.AbstractTerminalCommand;
014import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument;
015import gov.nist.secauto.metaschema.cli.processor.command.ICommandExecutor;
016import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
017import gov.nist.secauto.metaschema.core.metapath.ISequence;
018import gov.nist.secauto.metaschema.core.metapath.MetapathExpression;
019import gov.nist.secauto.metaschema.core.metapath.StaticContext;
020import gov.nist.secauto.metaschema.core.metapath.item.DefaultItemWriter;
021import gov.nist.secauto.metaschema.core.metapath.item.IItemWriter;
022import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem;
023import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItemFactory;
024import gov.nist.secauto.metaschema.core.model.IModule;
025import gov.nist.secauto.metaschema.core.model.MetaschemaException;
026import gov.nist.secauto.metaschema.core.util.CollectionUtil;
027import gov.nist.secauto.metaschema.core.util.ObjectUtils;
028import gov.nist.secauto.metaschema.core.util.UriUtils;
029import gov.nist.secauto.metaschema.databind.DefaultBindingContext;
030import gov.nist.secauto.metaschema.databind.IBindingContext;
031import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
032
033import org.apache.commons.cli.CommandLine;
034import org.apache.commons.cli.Option;
035import org.apache.logging.log4j.LogManager;
036import org.apache.logging.log4j.Logger;
037
038import java.io.IOException;
039import java.io.PrintWriter;
040import java.io.StringWriter;
041import java.io.Writer;
042import java.net.URI;
043import java.net.URISyntaxException;
044import java.nio.file.Files;
045import java.nio.file.Path;
046import java.nio.file.Paths;
047import java.util.Collection;
048import java.util.List;
049
050import edu.umd.cs.findbugs.annotations.NonNull;
051
052public class EvaluateMetapathCommand
053    extends AbstractTerminalCommand {
054  private static final Logger LOGGER = LogManager.getLogger(EvaluateMetapathCommand.class);
055
056  @NonNull
057  private static final String COMMAND = "eval";
058  @NonNull
059  private static final Option EXPRESSION_OPTION = ObjectUtils.notNull(
060      Option.builder("e")
061          .longOpt("expression")
062          .required()
063          .hasArg()
064          .argName("EXPRESSION")
065          .desc("Metapath expression to execute")
066          .build());
067  @NonNull
068  public static final Option CONTENT_OPTION = ObjectUtils.notNull(
069      Option.builder("i")
070          .hasArg()
071          .argName("FILE_OR_URL")
072          .desc("Metaschema content instance resource")
073          .build());
074
075  @NonNull
076  public static final Option METASCHEMA_OPTION = ObjectUtils.notNull(
077      Option.builder("m")
078          .hasArg()
079          .argName("FILE_OR_URL")
080          .desc("metaschema resource")
081          .build());
082
083  @Override
084  public String getName() {
085    return COMMAND;
086  }
087
088  @Override
089  public String getDescription() {
090    return "Execute a Metapath expression against a document";
091  }
092
093  @SuppressWarnings("null")
094  @Override
095  public Collection<? extends Option> gatherOptions() {
096    return List.of(
097        METASCHEMA_OPTION,
098        CONTENT_OPTION,
099        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}