code: /*
   * citygml-tools - Collection of tools for processing CityGML files
   * https://github.com/citygml4j/citygml-tools
   *
   * citygml-tools is part of the citygml4j project
   *
   * Copyright 2018-2023 Claus Nagel <claus.nagel@gmail.com>
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *     http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package org.citygml4j.tools.command;
  
  import org.citygml4j.tools.CityGMLTools;
  import org.citygml4j.tools.ExecutionException;
  import org.citygml4j.tools.log.LogLevel;
  import org.citygml4j.tools.option.InputOptions;
  import org.citygml4j.tools.util.InputFiles;
  import org.xml.sax.ErrorHandler;
  import org.xml.sax.SAXParseException;
  import org.xmlobjects.schema.SchemaHandler;
  import org.xmlobjects.schema.SchemaHandlerException;
  import org.xmlobjects.stream.XMLReader;
  import org.xmlobjects.stream.XMLReaderFactory;
  import picocli.CommandLine;
  
  import javax.xml.XMLConstants;
  import javax.xml.transform.stream.StreamSource;
  import javax.xml.validation.Schema;
  import javax.xml.validation.SchemaFactory;
  import javax.xml.validation.Validator;
  import java.net.URI;
  import java.nio.file.Path;
  import java.nio.file.Paths;
  import java.util.List;
  import java.util.Set;
  
  @CommandLine.Command(name = "validate",
          description = "Validates CityGML files against the CityGML XML schemas.")
  public class ValidateCommand extends CityGMLTool {
      @CommandLine.Option(names = {"-e", "--suppress-errors"},
              description = "Do not print validation error messages for a concise report.")
      private boolean suppressMessages;
  
      @CommandLine.Option(names = {"-s", "--schema"}, split = ",", paramLabel = "<URI>",
              description = "One or more files or URLs of additional XML schemas to use for the validation. " +
                      "Note that the official CityGML schemas cannot be replaced.")
      private Set<String> schemas;
  
      @CommandLine.Mixin
      private InputOptions inputOptions;
  
      @Override
      public Integer call() throws ExecutionException {
          log.debug("Searching for CityGML input files.");
          List<Path> inputFiles = InputFiles.of(inputOptions.getFiles()).find();
          if (inputFiles.isEmpty()) {
              log.warn("No files found at " + inputOptions.joinFiles() + ".");
              return CommandLine.ExitCode.OK;
          }
  
          log.info("Found " + inputFiles.size() + " file(s) at " + inputOptions.joinFiles() + ".");
  
          log.debug("Loading default CityGML schemas.");
          SchemaHandler rootHandler;
          try {
              rootHandler = getCityGMLContext().getDefaultSchemaHandler();
          } catch (SchemaHandlerException e) {
              throw new ExecutionException("Failed to load default CityGML schemas.", e);
          }
  
          if (schemas != null) {
              for (String schema : schemas) {
                  try {
                      Path schemaFile = CityGMLTools.WORKING_DIR.resolve(schema).toAbsolutePath();
                      log.debug("Reading additional XML schema from " + schemaFile + ".");
                      rootHandler.parseSchema(schema);
                  } catch (Exception e) {
                      throw new ExecutionException("Failed to read XML schema from " + schema + ".", e);
                  }
              }
          }
  
          ValidationErrorHandler errorHandler = new ValidationErrorHandler();
          int invalid = 0;
  
          for (int i = 0; i < inputFiles.size(); i++) {
              Path inputFile = inputFiles.get(i);
  
              log.info("[" + (i + 1) + "|" + inputFiles.size() + "] Validating file " + inputFile.toAbsolutePath() + ".");
  
              SchemaHandler schemaHandler = new ValidationSchemaHandler(rootHandler);
  
              // read more external schemas from schemaLocation attribute of root element
              try (XMLReader reader = XMLReaderFactory.newInstance(getCityGMLContext().getXMLObjects())
                      .withSchemaHandler(schemaHandler)
                      .createReader(inputFile, inputOptions.getEncoding())) {
                  reader.nextTag();
              } catch (Exception e) {
                  throw new ExecutionException("Failed to read file " + inputFile.toAbsolutePath() + ".", e);
              }
  
              try {
                  SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
                  Schema schema = schemaFactory.newSchema(schemaHandler.getSchemas());
  
                  Validator validator = schema.newValidator();
                  validator.setErrorHandler(errorHandler);
                  validator.validate(new StreamSource(inputFile.toFile()));
  
                  if (errorHandler.getErrors() == 0) {
                      log.info("The file is valid.");
                  } else {
                      log.warn("The file is invalid. Found " + errorHandler.getErrors() + " error(s).");
                      invalid++;
                  }
              } catch (Exception e) {
                  throw new ExecutionException("Failed to validate CityGML file.", e);
              } finally {
                  errorHandler.reset();
              }
          }
  
          if (invalid == 0) {
              log.info("Validation complete. All files are valid.");
              return CommandLine.ExitCode.OK;
          } else {
              log.warn("Validation complete. Found " + invalid + " invalid file(s).");
              return 3;
          }
      }
  
      private class ValidationSchemaHandler extends SchemaHandler {
  
          ValidationSchemaHandler(SchemaHandler other) {
              copy(other);
          }
  
          @Override
          public void parseSchema(String namespaceURI, String schemaLocation) throws SchemaHandlerException {
              if (!getTargetNamespaces().contains(namespaceURI)) {
                  String uri;
                  try {
                      uri = Paths.get(URI.create(schemaLocation)).toAbsolutePath().toString();
                  } catch (Exception e) {
                      uri = schemaLocation;
                  }
  
                  log.debug("Reading external XML schema from " + uri + ".");
              }
  
              super.parseSchema(namespaceURI, schemaLocation);
          }
      }
  
      private class ValidationErrorHandler implements ErrorHandler {
          private String location;
          private int errors;
  
          int getErrors() {
              return errors;
          }
  
          void reset() {
              location = null;
              errors = 0;
          }
  
          @Override
          public void warning(SAXParseException exception) {
              reportError(exception, "Warning", LogLevel.WARN);
          }
  
          @Override
          public void error(SAXParseException exception) {
              reportError(exception, "Invalid content", LogLevel.ERROR);
          }
  
          @Override
          public void fatalError(SAXParseException exception) {
              reportError(exception, "Invalid content", LogLevel.ERROR);
          }
  
          private void reportError(SAXParseException exception, String type, LogLevel level) {
              String location = exception.getLineNumber() + ", " + exception.getColumnNumber();
              if (!location.equals(this.location)) {
                  this.location = location;
                  errors++;
              }
  
              if (!suppressMessages) {
                  log.log(level, type + " at [" + location + "]: " + exception.getMessage());
              }
          }
      }
  }
  