/** This method search for the files mentioned in manifest inside PCI Package */
 private void validateManifestFilePaths() throws ValidationException {
   for (String fileName : manifestFilePaths) {
     if (!inputFilePaths.contains(fileName)) {
       reporter.error(messages.getMessage("msg.missedResource") + fileName);
     }
   }
 }
 private static String getErrorMessage(String key, Exception ex) {
   StringBuilder sb = new StringBuilder();
   sb.append(MANIFEST_FILENAME);
   sb.append(':');
   sb.append(' ');
   sb.append(messages.getMessage(key));
   sb.append(ex.getMessage());
   return sb.toString();
 }
 /**
  * This method search for the files which exist in PCI Package but are not mentioned in manifest
  */
 private void validateInputFilePaths() throws ValidationException {
   for (String packageFileName : inputFilePaths) {
     if (!manifestFilePaths.contains(packageFileName)) {
       if (!isPathAllowed(packageFileName)) {
         reporter.error(messages.getMessage("msg.unexpectedResource") + packageFileName);
       }
     }
   }
 }
 private void validatePaths(String[] paths) throws ValidationException, PciApiException {
   scanInputPaths(paths);
   if (!manifestExists) {
     throw new ValidationException(messages.getMessage("msg.missingManifest"));
   }
   scanManifestPaths(MANIFEST_FILENAME);
   validateManifestFilePaths();
   validateInputFilePaths();
 }
/** PackageStructureValidator validates Folders in the package against manifest.xml */
public class PackageStructureValidator implements Validator {

  private static final Messages messages = Messages.getInstance();

  /**
   * Normal path separator character. This character is always used as a path separator regardless
   * of the system path separator.
   *
   * @see #normalizeFilePath(String)
   */
  public static final char PATH_SEPARATOR_CHAR = '/';

  /**
   * The name of the PCI pack manifest file. This name is used to find the manifest in the list of
   * input file paths.
   *
   * @see #inputFilePaths
   */
  public static final String MANIFEST_FILENAME = "manifest.xml";

  /**
   * The name of a parameter that specifies the path to a catalog file which will be used to resolve
   * external resources during XML parsing.
   */
  public static final String PARAM_PATH_CATALOG = "path.catalog";

  /**
   * The name of a parameter that specifies whether the folder paths (the paths that don't end with
   * a file name) are allowed.
   *
   * <p>Originally, this parameter was used only with Interchange, that's where the parameter comes
   * from. This parameter is be renamed to something like "allow.folders" in the future.
   */
  public static final String PARAM_ALLOW_INTERCHANGE = "allow.interchange";

  /**
   * Contains input file paths. All paths in this collection are normalized. Intended to store file
   * paths relative to a PCI pack's root.
   *
   * @see #normalizeFilePath(String)
   */
  private List<String> inputFilePaths = new ArrayList<String>();

  /**
   * Contains relative file paths read from the manifest.xml including manifest.xml. This collection
   * is actually contains all the expected file paths for the given pack. All paths in this
   * collection are normalized.
   *
   * @see #normalizeFilePath(String)
   */
  private List<String> manifestFilePaths = new ArrayList<String>();

  /**
   * Specifies whether to allow interchange file resources in the input pack. If this field set to
   * <code>true</code>, all interchange files found in the pack won't be treated as unexpected
   * assuming that the manifest contains <code>add-replace</code> element specifying interchange
   * folder aspect validation. Basically this field contains the value of the {@link
   * #PARAM_ALLOW_INTERCHANGE} parameter, and by default is set to <code>false</code>.
   */
  private boolean allowInterchange = false;

  /**
   * List of folder paths extracted from the manifest.xml file. These paths come from add-replace
   * elements with aspect="folder".
   *
   * <p>This list gets populated only if the #allowInterchange property is <code>true</code>
   *
   * @see #allowInterchange
   * @see #manifestFolderPaths
   */
  private List<String> manifestFolderPaths = new ArrayList<String>();

  /**
   * Identifies whether the manifest.xml file path is presented in the input file paths list.
   *
   * @see #MANIFEST_FILENAME
   */
  private boolean manifestExists = false;

  /**
   * a <code>CatalogManager</code> instance that will be used to resolve external resource
   * references during parsing of manifest.xml. This instance is configured only once per the
   * validate() method call.
   *
   * @see #configureCatalogManager(String)
   * @see #PARAM_PATH_CATALOG
   */
  private CatalogManager catalogManager = null;

  /** The orchestration context passed to the validator */
  private OrchestrationContext context;

  /** <code>Reporter</code> instance passed to the validator */
  private Reporter reporter;

  /**
   * Initializes the main properties of the validator.
   *
   * @param context an <code>OrchestrationContext</code> instance passed in the validator
   * @param params a <code>Params</code> data passed in the validator
   * @param reporter a reporter passed in the validator
   * @see #validate(OrchestrationContext, Params, Reporter, String...)
   */
  private void init(OrchestrationContext context, Params params, Reporter reporter)
      throws ConfigurationResourceAccessException {
    this.reporter = reporter;
    this.context = context;
    applyParams(params);
  }

  /**
   * Initializes properties with respect to the parameters passed to the validator.
   *
   * <p>Currently, this method initializes only the allowInterchange and catalogManager properties
   *
   * @param params a <code>Params</code> instance to apply
   * @see #allowInterchange
   * @see #catalogManager
   */
  private void applyParams(Params params) throws ConfigurationResourceAccessException {
    if (params == null) {
      // can be null in unit tests!
      return;
    }
    allowInterchange = Boolean.parseBoolean(params.getParam(PARAM_ALLOW_INTERCHANGE));
    catalogManager = configureCatalogManager(params.getParam(PARAM_PATH_CATALOG));
  }

  private CatalogManager configureCatalogManager(String catalogFilePath)
      throws ConfigurationResourceAccessException {
    CatalogManager catalogManager = new CatalogManager();
    catalogManager.setRelativeCatalogs(false);
    catalogManager.setPreferPublic(true);
    catalogManager.setUseStaticCatalog(false);
    catalogManager.setAllowOasisXMLCatalogPI(true);
    if (isNotEmpty(catalogFilePath)) {
      catalogManager.setCatalogFiles(resolvePath(catalogFilePath));
    }
    return catalogManager;
  }

  /**
   * Method to validate extracted package against folder structure Assumption is that manifest.xml
   * file was validated against schema
   *
   * @param context an {@link OrchestrationContext} instance which will be used for retrieving
   *     configuration and input file resources
   * @param params validation name-value parameter pairs
   * @param paths an array of input file paths to validate the pack structure
   * @param reporter an instance of {@link Reporter} which will apply all error messages reported by
   *     this validator
   */
  @Override
  public void validate(
      OrchestrationContext context, Params params, Reporter reporter, String... paths)
      throws ValidationException {
    try {
      init(context, params, reporter);
      validatePaths(paths);
    } catch (Exception e) {
      reporter.error(e.getMessage());
    }
  }

  private void validatePaths(String[] paths) throws ValidationException, PciApiException {
    scanInputPaths(paths);
    if (!manifestExists) {
      throw new ValidationException(messages.getMessage("msg.missingManifest"));
    }
    scanManifestPaths(MANIFEST_FILENAME);
    validateManifestFilePaths();
    validateInputFilePaths();
  }

  private void scanInputPaths(String[] paths) {
    for (String path : paths) {
      if (!isDirectory(path)) {
        if (MANIFEST_FILENAME.equals(path)) {
          manifestExists = true;
        } else {
          inputFilePaths.add(normalizeFilePath(path));
        }
      }
    }
  }

  private void scanManifestPaths(String manifestPath) throws ValidationException, PciApiException {
    PciPackage pciPackage = getPciPackage(getInputStream(manifestPath));
    if (allowInterchange) {
      scanManifestPathsAllowFolder(pciPackage);
    } else {
      scanManifestPaths(pciPackage);
    }
  }

  /**
   * Extracts all the paths defined in the package's manifest.xml.
   *
   * @param pciPackage a <code>PciPackage</code> instance to scan for the manifest files
   * @see #manifestFilePaths
   */
  private void scanManifestPaths(PciPackage pciPackage) {
    Collection<Action> actions = pciPackage.getAddReplaceActions();
    for (Action action : actions) {
      manifestFilePaths.add(action.getAddReplace().getObjectMCPPathAttr());
    }
  }

  /**
   * Extracts all the paths defined in the package's manifest.xml; additionally it recognizes the
   * folder paths (the paths defined in an add-replace element with aspect="folder".
   *
   * @param pciPackage
   * @see #inputFilePaths
   * @see #manifestFolderPaths
   * @see #hasFolderAspect(com.wolterskluwer.pci.schema.v3.Action)
   */
  private void scanManifestPathsAllowFolder(PciPackage pciPackage) {
    Collection<Action> actions = pciPackage.getAddReplaceActions();
    for (Action action : actions) {
      String path = action.getAddReplace().getObjectMCPPathAttr();
      if (hasFolderAspect(action)) {
        manifestFolderPaths.add(path);
      } else {
        manifestFilePaths.add(path);
      }
    }
  }

  /**
   * Determines whether the given Action instance has an add-replace element with aspect="folder".
   *
   * @param action an <code>Action</code> instant to test
   * @return
   */
  private static boolean hasFolderAspect(Action action) {
    return "folder".equals(action.getAddReplace().getAspect().value());
  }

  private PciPackage getPciPackage(InputStream manifestStream)
      throws PciApiException, ValidationException {
    PciPackage pciPackage = new PciPackage();
    try {
      Manifest manifest = PciPackage.readManifest(manifestStream, catalogManager);
      // in order to be abe to call PciPackage instance methods it's important to set the
      // manifest property because the the readManifest() method doesn't set this property,
      // it just returns a reference to the newly created Manifest instance
      pciPackage.setManifest(manifest);
    } catch (PciApiException ex) {
      throw new ValidationException(getErrorMessage("msg.xml.parseError", ex), ex);
    } finally {
      IOUtils.closeQuietly(manifestStream);
    }
    return pciPackage;
  }

  /**
   * Turns a relative path into its absolute representation
   *
   * @param path
   * @return an absolute path
   * @throws ConfigurationResourceAccessException
   */
  private String resolvePath(String path) throws ConfigurationResourceAccessException {
    return context.getConfiguration().getFileResource(path).getAbsolutePath();
  }

  private InputStream getInputStream(String path) throws ValidationException {
    return context.getInputStream(path);
  }

  private boolean isPathAllowed(String path) {
    for (String directoryPath : manifestFolderPaths) {
      if (path.startsWith(directoryPath)) {
        return true;
      }
    }
    return false;
  }

  /** This method search for the files mentioned in manifest inside PCI Package */
  private void validateManifestFilePaths() throws ValidationException {
    for (String fileName : manifestFilePaths) {
      if (!inputFilePaths.contains(fileName)) {
        reporter.error(messages.getMessage("msg.missedResource") + fileName);
      }
    }
  }

  /**
   * This method search for the files which exist in PCI Package but are not mentioned in manifest
   */
  private void validateInputFilePaths() throws ValidationException {
    for (String packageFileName : inputFilePaths) {
      if (!manifestFilePaths.contains(packageFileName)) {
        if (!isPathAllowed(packageFileName)) {
          reporter.error(messages.getMessage("msg.unexpectedResource") + packageFileName);
        }
      }
    }
  }

  private static String getErrorMessage(String key, Exception ex) {
    StringBuilder sb = new StringBuilder();
    sb.append(MANIFEST_FILENAME);
    sb.append(':');
    sb.append(' ');
    sb.append(messages.getMessage(key));
    sb.append(ex.getMessage());
    return sb.toString();
  }

  /**
   * Replaces all occurrences of <code>\</code> (backslash) with the normal {@link
   * #PATH_SEPARATOR_CHAR}.
   *
   * @param filePath a file path to normalize
   * @return normalized path
   */
  private static String normalizeFilePath(String filePath) {
    if (filePath.indexOf('\\') != -1) {
      filePath = filePath.replace('\\', PATH_SEPARATOR_CHAR);
    }
    return filePath;
  }

  private static boolean isDirectory(String path) {
    char lastChar = path.charAt(path.length() - 1);
    return lastChar == PATH_SEPARATOR_CHAR || lastChar == '\\';
  }
}