/*
 * Copyright (c) 2016-2017 by OpenText Corporation. All Rights Reserved.
 */
package com.opentext.ia.sdk.sip;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.function.Supplier;

import com.opentext.ia.sdk.support.io.FileBuffer;
import com.opentext.ia.sdk.support.io.FileSupplier;

/**
 * Assemble a batch of SIPs, based on some sort of {@linkplain SipSegmentationStrategy segmentation strategy} that
 * determines which domain objects go into which SIPs.
 *

 * To create a batch of SIPs, simply {@linkplain #add(Object) add} domain objects and {@linkplain #end() end} the
 * assembly process. Then access the generated files and metrics about them using {@linkplain #getSipsMetrics()}.
 *

 * There are several {@linkplain SipSegmentationStrategy factory methods} available to create common segmentation
 * strategies and you can also {@linkplain SipSegmentationStrategy#combining(SipSegmentationStrategy...) combine} them.
 *
 *

Warning:

This object is not thread-safe. If you want to use multiple threads to assemble SIPs, let
 * each use their own instance.

 * @param <D> The type of domain object to assemble SIPs from
 */
public class BatchSipAssembler<D> {

  private final SipAssembler<D> assembler;
  private final SipSegmentationStrategy<D> segmentationStrategy;
  private final Supplier<File> fileSupplier;
  private final Collection<FileGenerationMetrics> sipsMetrics = new ArrayList<>();
  private File current;

  /**
   * Create an instance that assembles SIPs in a temporary directory.
   * @param assembler An assembler that builds up the SIPs in the batch
   * @param segmentationStrategy A strategy that determines when to start new SIPs
   */
  public BatchSipAssembler(SipAssembler<D> assembler, SipSegmentationStrategy<D> segmentationStrategy) {
    this(assembler, segmentationStrategy, FileSupplier.fromTemporaryDirectory());
  }

  /**
   * Create an instance that assembles SIPs in the given directory.
   * @param assembler An assembler that builds up the SIPs in the batch
   * @param segmentationStrategy A strategy that determines when to start new SIPs
   * @param dir Directory in which to generate SIP files
   */
  public BatchSipAssembler(SipAssembler<D> assembler, SipSegmentationStrategy<D> segmentationStrategy, File dir) {
    this(assembler, segmentationStrategy, FileSupplier.fromDirectory(dir));
  }

  /**
   * Create an instance that assembles SIPs using the given supplier of SIP files.
   * @param assembler An assembler that builds up the SIPs in the batch
   * @param segmentationStrategy A strategy that determines when to start new SIPs
   * @param fileSupplier A supplier of files in which to store the SIPs
   */
  public BatchSipAssembler(SipAssembler<D> assembler, SipSegmentationStrategy<D> segmentationStrategy,
      Supplier<File> fileSupplier) {
    this.assembler = assembler;
    this.segmentationStrategy = segmentationStrategy;
    this.fileSupplier = fileSupplier;
    setFinalSipInDss(false);
  }

  protected final void setFinalSipInDss(boolean finalSipInDss) {
    assembler.getPackagingInformationFactory().setFinalSipInDss(finalSipInDss);
  }

  /**
   * Add a domain object to the batch of SIPs.
   * @param domainObject The domain object to add
   * @throws IOException When an I/O error occurs
   */
  public synchronized void add(D domainObject) throws IOException {
    if (shouldStartNewSip(domainObject)) {
      startSip();
    }
    assembler.add(domainObject);
  }

  private boolean shouldStartNewSip(D component) {
    // NOTE: Give the segmentation strategy to prevent the component from being added by throwing an exception
    return segmentationStrategy.shouldStartNewSip(component, assembler.getMetrics()) || current == null;
  }

  private void startSip() throws IOException {
    closeCurrentSip();
    startNewSip();
  }

  protected final synchronized void closeCurrentSip() throws IOException {
    if (current != null) {
      assembler.end();
      FileGenerationMetrics metrics = new FileGenerationMetrics(current, assembler.getMetrics());
      current = null;
      sipEnded(metrics);
    }
  }

  protected void sipEnded(FileGenerationMetrics metrics) {
    sipsMetrics.add(metrics);
  }

  private synchronized void startNewSip() throws IOException {
    File file = fileSupplier.get();
    assembler.start(new FileBuffer(file));
    // NOTE: Set *after* [assembler] has started, since we check [current] to determine whether
    // [assembler] has started.
    current = file;
  }

  /**
   * End the batch assembly process.
   * @throws IOException When an I/O error occurs
   */
  public synchronized void end() throws IOException {
    setFinalSipInDss(true);
    closeCurrentSip();
  }

  /**
   * Returns the files that were generated as part of this batch and metrics about the SIPs stored in them.
   * @return The files that were generated as part of this batch and metrics about the SIPs stored in them
   */
  public Collection<FileGenerationMetrics> getSipsMetrics() {
    return Collections.unmodifiableCollection(sipsMetrics);
  }

}


/*
 * Copyright (c) 2016-2017 by OpenText Corporation. All Rights Reserved.
 */
package com.opentext.ia.configuration;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.json.JSONObject;

/**
 * An object being configured.
 * @author Ray Sinnema
 * @since 9.4.0
 */
public class ConfigurationObject {

  private static final String NL = System.lineSeparator();
  private final JSONObject properties = new JSONObject();
  private final Map<String, List<ConfigurationObject>> childObjects = new HashMap<>();
  private final String type;

  public ConfigurationObject(String type) {
    this.type = type;
  }

  /**
   * Returns the object's properties as JSON.
   * @return The object's properties as JSON
   */
  public JSONObject getProperties() {
    return properties;
  }

  /**
   * Returns the objects owned by this object.
   * @return The objects owned by this object
   */
  public Map<String, List<ConfigurationObject>> getChildObjects() {
    return childObjects;
  }

  /**
   * Returns the object's type.
   * @return The object's type
   */
  public String getType() {
    return type;
  }

  /**
   * Set the value of a property.
   * @param name The name of the property to set
   * @param value The value to set the property to
   */
  public void setProperty(String name, Object value) {
    properties.put(name, toJsonValue(value));
  }

  @SuppressWarnings("rawtypes")
  private Object toJsonValue(Object value) {
    if (value == null) {
      return JSONObject.NULL;
    }
    return value;
  }

  public boolean hasProperty(String name) {
    return properties.has(name) && properties.get(name) != JSONObject.NULL;
  }

  /**
   * Add an object that is to be owned by this object.
   * @param collection The collection to store the child object under
   * @param childObject The child object to add
   */
  public void addChildObject(String collection, ConfigurationObject childObject) {
    childObjects.computeIfAbsent(collection, ignored -> new ArrayList<>()).add(childObject);
  }

  @Override
  public String toString() {
    return toString("");
  }

  private String toString(String indent) {
    StringBuilder result = new StringBuilder();
    result.append(indent).append("properties:").append(NL).append(indent).append("  ")
        .append(properties.toString(4 + indent.length())).append(NL);

    childObjects.forEach((collection, objects) -> {
      result.append(indent).append(collection).append(':').append(NL);
      if (objects.isEmpty()) {
        result.append(indent).append("  []").append(NL);
      } else {
        objects.forEach(object ->
            result.append(indent).append(object.toString(indent + "  ")).append(NL));
      }
    });

    return result.toString();
  }

}


/*
 * Copyright (c) 2016-2017 by OpenText Corporation. All Rights Reserved.
 */
package com.opentext.ia.sdk.support.datetime;

import java.util.Calendar;
import java.util.Date;

import javax.annotation.Nullable;
import javax.xml.bind.DatatypeConverter;

/**
 * Utilities for working with dates.
 */
public final class Dates {

  private Dates() {
    // Utility class
  }

  /**
   * Convert a date to ISO 8601 dateTime format.
   * @param dateTime The datetime to convert
   * @return The dateTime in ISO format
   */
  @Nullable
  public static String toIso(Date dateTime) {
    if (dateTime == null) {
      return null;
    }
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(dateTime);
    return DatatypeConverter.printDateTime(calendar);
  }

  /**
   * Convert an ISO 8601 dateTime string to a date.
   * @param dateTime The ISO dateTime to convert
   * @return The converted dateTime
   */
  @Nullable
  public static Date fromIso(String dateTime) {
    if (dateTime == null) {
      return null;
    }
    return DatatypeConverter.parseDateTime(dateTime)
      .getTime();
  }

}


/*
 * Copyright (c) 2016-2017 by OpenText Corporation. All Rights Reserved.
 */
package com.opentext.ia.sdk.support.io;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.UUID;
import java.util.function.Supplier;

/**
 * Several ways of supplying files.
 */
public final class FileSupplier {

  private FileSupplier() {
    // Utility class
  }

  /**
   * Returns a supplier that creates randomly named files in a temporary directory.
   * @return A supplier that creates randomly named files in a temporary directory
   */
  public static Supplier<File> fromTemporaryDirectory() {
    return fromDirectory(tempDirectory());
  }

  private static File tempDirectory() {
    try {
      return Files.createTempDirectory(null)
        .toFile();
    } catch (IOException e) {
      throw new RuntimeIoException(e);
    }
  }

  /**
   * Returns a supplier that creates randomly named files in the given directory.
   * @param dir The directory in which to create files
   * @return A supplier that creates randomly named files in the given directory
   */
  public static Supplier<File> fromDirectory(File dir) {
    return () -> createFileIn(dir);
  }

  private static File createFileIn(File dir) {
    return new File(ensureDir(dir), UUID.randomUUID()
      .toString());
  }

  private static File ensureDir(File dir) {
    if (!dir.isDirectory() && !dir.mkdirs()) {
      throw new IllegalArgumentException("Cannot create directory: " + dir);
    }
    return dir;
  }

  /**
   * Returns a supplier that creates sequentially named files in the given directory.
   * @param dir The directory in which to create files
   * @param prefix The prefix for the file names
   * @param suffix The suffix for the file names
   * @return A supplier that creates sequentially named files in the given directory
   */
  public static Supplier<File> fromDirectory(File dir, String prefix, String suffix) {
    return new Supplier<File>() {
      private int count;

      @Override
      public File get() {
        return new File(ensureDir(dir), String.format("%s%d%s", prefix, ++count, suffix));
      }
    };
  }

}


/*
 * Copyright (c) 2016-2017 by OpenText Corporation. All Rights Reserved.
 */
package com.opentext.ia.sdk.sip;

import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;

import com.opentext.ia.sdk.support.io.DataBuffer;
import com.opentext.ia.sdk.support.io.DataBufferSupplier;
import com.opentext.ia.sdk.support.io.DefaultZipAssembler;
import com.opentext.ia.sdk.support.io.EncodedHash;
import com.opentext.ia.sdk.support.io.FileBuffer;
import com.opentext.ia.sdk.support.io.HashAssembler;
import com.opentext.ia.sdk.support.io.IOStreams;
import com.opentext.ia.sdk.support.io.MemoryBuffer;
import com.opentext.ia.sdk.support.io.NoHashAssembler;
import com.opentext.ia.sdk.support.io.RuntimeIoException;
import com.opentext.ia.sdk.support.io.ZipAssembler;

/**
 * Assembles a Submission Information Package
 * (SIP) from several domain objects of the same type. Each domain object is typically a Plain Old Java Object (POJO)
 * that you create in an application-specific manner.
 *

 * A SIP is a ZIP file that contains:
 *

 *
One {@linkplain com.opentext.ia.sdk.sip.PackagingInformation Packaging Information} that describes the
 * content of the SIP

 *
One Preservation Description Information (PDI) that contains structured data to be archived

 *
Zero or more Content Data Objects that contain unstructured data to be archived. These are referenced from the
 * PDI

 *

 *

 * Packaging Information is created by a {@linkplain PackagingInformationFactory factory}. If you want only one SIP in a
 * {@linkplain DataSubmissionSession DSS}, then you can use a {@linkplain DefaultPackagingInformationFactory} to create
 * the Packaging Information based on a prototype which contains application-specific fields.
 *

 * The PDI will be assembled from the domain objects by an {@linkplain Assembler} and added to the ZIP by a
 * {@linkplain ZipAssembler}. Each domain object may also contain zero or more {@linkplain DigitalObject}s, which are
 * extracted from the domain object using a {@linkplain DigitalObjectsExtraction} and added to the ZIP. The PDI is
 * written to a {@linkplain DataBuffer} until it is complete. For small PDIs, you can use a {@linkplain MemoryBuffer} to
 * hold this data, but for larger PDIs you should use a {@linkplain FileBuffer} to prevent running out of memory.
 *

 * Use the following steps to assemble a SIP:
 *

 *
Start the process by calling the {@linkplain #start(DataBuffer)} method

 *
Add zero or more domain objects by calling the {@linkplain #add(Object)} method multiple times

 *
Finish the process by calling the {@linkplain #end()} method

 *

 * You can optionally get metrics about the SIP assembly process by calling {@linkplain #getMetrics()} at any time.
 *

 * If the number of domain objects is small and each individual domain object is also small, you can wrap a
 * {@linkplain SipAssembler} in a {@linkplain Generator} to reduce the above code to a single call.
 *

 * To assemble a number of SIPs in a batch, use {@linkplain BatchSipAssembler}.
 *
 *

Warning:

This object is not thread-safe. If you want to use multiple threads to assemble SIPs, let
 * each use their own instance.

 * @param <D> The type of domain objects to assemble the SIP from
 */
public class SipAssembler<D> implements Assembler<D> {

  private static final String PACKAGING_INFORMATION_ENTRY = "eas_sip.xml";
  private static final String PDI_ENTRY = "eas_pdi.xml";

  private final ZipAssembler zip;
  private final Assembler<PackagingInformation> packagingInformationAssembler;
  private final Assembler<HashedContents<D>> pdiAssembler;
  private final HashAssembler pdiHashAssembler;
  private final Supplier<? extends DataBuffer> pdiBufferSupplier;
  private final PackagingInformationFactory packagingInformationFactory;
  private final Counters metrics = new Counters();
  private final ContentAssembler<D> contentAssembler;
  private DataBuffer pdiBuffer;
  private DataBuffer sipFileBuffer;
  private Optional<EncodedHash> pdiHash;

  /**
   * Assemble a SIP that contains only structured data and is the only SIP in its DSS.
   * @param <D> The type of domain objects to assemble the SIP from
   * @param prototype Prototype for the Packaging Information
   * @param pdiAssembler Assembler that builds up the PDI
   * @return The newly created SIP assembler
   */
  public static <D> SipAssembler<D> forPdi(PackagingInformation prototype, Assembler<HashedContents<D>> pdiAssembler) {
    return forPdiWithHashing(prototype, pdiAssembler, new NoHashAssembler());
  }

  /**
   * Assemble a SIP that contains only structured data and is the only SIP in its DSS.
   * @param <D> The type of domain objects to assemble the SIP from
   * @param factory Factory for creating the Packaging Information
   * @param pdiAssembler Assembler that builds up the PDI
   * @return The newly created SIP assembler
   */
  public static <D> SipAssembler<D> forPdi(PackagingInformationFactory factory,
      Assembler<HashedContents<D>> pdiAssembler) {
    return forPdiWithHashing(factory, pdiAssembler, new NoHashAssembler());
  }

  /**
   * Assemble a SIP that contains only structured data and is the only SIP in its DSS.
   * @param <D> The type of domain objects to assemble the SIP from
   * @param prototype Prototype for the Packaging Information
   * @param pdiAssembler Assembler that builds up the PDI
   * @param pdiHashAssembler Assembler that builds up an encoded hash for the PDI
   * @return The newly created SIP assembler
   */
  public static <D> SipAssembler<D> forPdiWithHashing(PackagingInformation prototype,
      Assembler<HashedContents<D>> pdiAssembler, HashAssembler pdiHashAssembler) {
    return forPdiAndContentWithHashing(prototype, pdiAssembler, pdiHashAssembler, ContentAssembler.ignoreContent());
  }

  /**
   * Assemble a SIP that contains only structured data and is the only SIP in its DSS.
   * @param <D> The type of domain objects to assemble the SIP from
   * @param factory Factory for creating the Packaging Information
   * @param pdiAssembler Assembler that builds up the PDI
   * @param pdiHashAssembler Assembler that builds up an encoded hash for the PDI
   * @return The newly created SIP assembler
   */
  public static <D> SipAssembler<D> forPdiWithHashing(PackagingInformationFactory factory,
      Assembler<HashedContents<D>> pdiAssembler, HashAssembler pdiHashAssembler) {
    return forPdiAndContentWithHashing(factory, pdiAssembler, pdiHashAssembler, ContentAssembler.ignoreContent());
  }

  /**
   * Assemble a SIP that contains only structured data and is the only SIP in its DSS.
   * @param <D> The type of domain objects to assemble the SIP from
   * @param prototype Prototype for the Packaging Information
   * @param pdiAssembler Assembler that builds up the PDI
   * @param contentsExtraction Extraction of content from domain objects added to the SIP
   * @return The newly created SIP assembler
   */
  public static <D> SipAssembler<D> forPdiAndContent(PackagingInformation prototype,
      Assembler<HashedContents<D>> pdiAssembler, DigitalObjectsExtraction<D> contentsExtraction) {
    HashAssembler noHashAssembler = new NoHashAssembler();
    return forPdiAndContentWithHashing(prototype, pdiAssembler, noHashAssembler, contentsExtraction, noHashAssembler);
  }

  /**
   * Assemble a SIP that contains only structured data and is the only SIP in its DSS.
   * @param <D> The type of domain objects to assemble the SIP from
   * @param prototype Prototype for the Packaging Information
   * @param pdiAssembler Assembler that builds up the PDI
   * @param contentAssembler ContentAssembler that adds the digital objects to the SIP
   * @return The newly created SIP assembler
   */
  public static <D> SipAssembler<D> forPdiAndContent(PackagingInformation prototype,
      Assembler<HashedContents<D>> pdiAssembler, ContentAssembler<D> contentAssembler) {
    HashAssembler noHashAssembler = new NoHashAssembler();
    return forPdiAndContentWithHashing(prototype, pdiAssembler, noHashAssembler, contentAssembler);
  }

  /**
   * Assemble a SIP that contains only structured data and is the only SIP in its DSS.
   * @param <D> The type of domain objects to assemble the SIP from
   * @param factory Factory for creating the Packaging Information
   * @param pdiAssembler Assembler that builds up the PDI
   * @param contentAssembler ContentAssembler that adds the digital objects to the SIP
   * @return The newly created SIP assembler
   */
  public static <D> SipAssembler<D> forPdiAndContent(PackagingInformationFactory factory,
      Assembler<HashedContents<D>> pdiAssembler, ContentAssembler<D> contentAssembler) {
    HashAssembler noHashAssembler = new NoHashAssembler();
    return forPdiAndContentWithHashing(factory, pdiAssembler, noHashAssembler, contentAssembler);
  }

  /**
   * Assemble a SIP that contains only structured data and is the only SIP in its DSS.
   * @param <D> The type of domain objects to assemble the SIP from
   * @param factory Factory for creating the Packaging Information
   * @param pdiAssembler Assembler that builds up the PDI
   * @param contentsExtraction Extraction of content from domain objects added to the SIP
   * @return The newly created SIP assembler
   */
  public static <D> SipAssembler<D> forPdiAndContent(PackagingInformationFactory factory,
      Assembler<HashedContents<D>> pdiAssembler, DigitalObjectsExtraction<D> contentsExtraction) {
    HashAssembler noHashAssembler = new NoHashAssembler();
    return forPdiAndContentWithHashing(factory, pdiAssembler, noHashAssembler,
        ContentAssembler.noDedup(contentsExtraction, noHashAssembler));
  }

  /**
   * Assemble a SIP that contains only structured data and is the only SIP in its DSS.
   * @param <D> The type of domain objects to assemble the SIP from
   * @param prototype Prototype for the Packaging Information
   * @param pdiAssembler Assembler that builds up the PDI
   * @param contentsExtraction Extraction of content from domain objects added to the SIP
   * @param contentHashAssembler Assembler that builds up an encoded hash for the extracted content
   * @return The newly created SIP assembler
   */
  public static <D> SipAssembler<D> forPdiAndContentWithContentHashing(PackagingInformation prototype,
      Assembler<HashedContents<D>> pdiAssembler, DigitalObjectsExtraction<D> contentsExtraction,
      HashAssembler contentHashAssembler) {
    return forPdiAndContentWithHashing(prototype, pdiAssembler, new NoHashAssembler(), contentsExtraction,
        contentHashAssembler);
  }

  /**
   * Assemble a SIP that contains only structured data and is the only SIP in its DSS.
   * @param <D> The type of domain objects to assemble the SIP from
   * @param factory Factory for creating the Packaging Information
   * @param pdiAssembler Assembler that builds up the PDI
   * @param contentsExtraction Extraction of content from domain objects added to the SIP
   * @param contentHashAssembler Assembler that builds up an encoded hash for the extracted content
   * @return The newly created SIP assembler
   */
  public static <D> SipAssembler<D> forPdiAndContentWithContentHashing(PackagingInformationFactory factory,
      Assembler<HashedContents<D>> pdiAssembler, DigitalObjectsExtraction<D> contentsExtraction,
      HashAssembler contentHashAssembler) {
    return forPdiAndContentWithHashing(factory, pdiAssembler, new NoHashAssembler(),
        new ContentAssemblerDefault<D>(contentsExtraction, contentHashAssembler));
  }

  /**
   * Assemble a SIP that is the only SIP in its DSS.
   * @param <D> The type of domain objects to assemble the SIP from
   * @param prototype Prototype for the Packaging Information
   * @param pdiAssembler Assembler that builds up the PDI
   * @param pdiHashAssembler Assembler that builds up an encoded hash for the PDI
   * @param contentsExtraction Extraction of content from domain objects added to the SIP
   * @param contentHashAssembler Assembler that builds up an encoded hash for the extracted content
   * @return The newly created SIP assembler
   */
  public static <D> SipAssembler<D> forPdiAndContentWithHashing(PackagingInformation prototype,
      Assembler<HashedContents<D>> pdiAssembler, HashAssembler pdiHashAssembler,
      DigitalObjectsExtraction<D> contentsExtraction, HashAssembler contentHashAssembler) {
    return new SipAssembler<>(new DefaultPackagingInformationFactory(prototype), pdiAssembler, pdiHashAssembler,
        new DataBufferSupplier<>(MemoryBuffer.class),
        new ContentAssemblerDefault<D>(contentsExtraction, contentHashAssembler));
  }

  /**
   * Assemble a SIP that is the only SIP in its DSS.
   * @param <D> The type of domain objects to assemble the SIP from
   * @param prototype Prototype for the Packaging Information
   * @param pdiAssembler Assembler that builds up the PDI
   * @param pdiHashAssembler Assembler that builds up an encoded hash for the PDI
   * @param contentAssembler ContentAssembler that adds the digital objects to the SIP
   * @return The newly created SIP assembler
   */
  public static <D> SipAssembler<D> forPdiAndContentWithHashing(PackagingInformation prototype,
      Assembler<HashedContents<D>> pdiAssembler, HashAssembler pdiHashAssembler, ContentAssembler<D> contentAssembler) {
    return new SipAssembler<>(new DefaultPackagingInformationFactory(prototype), pdiAssembler, pdiHashAssembler,
        new DataBufferSupplier<>(MemoryBuffer.class), contentAssembler);
  }

  /**
   * Assemble a SIP that is the only SIP in its DSS.
   * @param <D> The type of domain objects to assemble the SIP from
   * @param factory Factory for creating the Packaging Information
   * @param pdiAssembler Assembler that builds up the PDI
   * @param pdiHashAssembler Assembler that builds up an encoded hash for the PDI
   * @param contentAssembler ContentAssembler that adds the digital objects to the SIP
   * @return The newly created SIP assembler
   */
  public static <D> SipAssembler<D> forPdiAndContentWithHashing(PackagingInformationFactory factory,
      Assembler<HashedContents<D>> pdiAssembler, HashAssembler pdiHashAssembler, ContentAssembler<D> contentAssembler) {
    return new SipAssembler<>(factory, pdiAssembler, pdiHashAssembler, new DataBufferSupplier<>(MemoryBuffer.class),
        contentAssembler);
  }

  /**
   * Create a new instance.
   * @param packagingInformationFactory Factory for creating the Packaging Information
   * @param pdiAssembler Assembler that builds up the PDI
   * @param pdiHashAssembler Assembler that builds up an encoded hash for the PDI and the unstructured data
   * @param pdiBufferSupplier Supplier for a data buffer to store the PDI
   * @param contentAssembler ContentAssembler that adds the digital objects to the SIP
   */
  public SipAssembler(PackagingInformationFactory packagingInformationFactory,
      Assembler<HashedContents<D>> pdiAssembler, HashAssembler pdiHashAssembler,
      Supplier<? extends DataBuffer> pdiBufferSupplier, ContentAssembler<D> contentAssembler) {
    this(packagingInformationFactory, new InfoArchivePackagingInformationAssembler(), pdiAssembler, pdiHashAssembler,
        pdiBufferSupplier, new DefaultZipAssembler(), contentAssembler);
  }

  SipAssembler(PackagingInformationFactory packagingInformationFactory,
      Assembler<PackagingInformation> packagingInformationAssembler, Assembler<HashedContents<D>> pdiAssembler,
      HashAssembler pdiHashAssembler, Supplier<? extends DataBuffer> pdiBufferSupplier, ZipAssembler zipAssembler,
      ContentAssembler<D> contentAssembler) {
    this.packagingInformationFactory = packagingInformationFactory;
    this.packagingInformationAssembler = packagingInformationAssembler;
    this.pdiAssembler = pdiAssembler;
    this.pdiHashAssembler = pdiHashAssembler;
    this.pdiBufferSupplier = pdiBufferSupplier;
    this.contentAssembler = contentAssembler;
    this.zip = zipAssembler;
  }

  @Override
  public synchronized void start(DataBuffer buffer) throws IOException {
    this.sipFileBuffer = buffer;
    pdiHash = Optional.empty();
    metrics.reset();
    metrics.set(SipMetrics.ASSEMBLY_TIME, System.currentTimeMillis());
    zip.begin(sipFileBuffer.openForWriting());
    contentAssembler.begin(zip, metrics);
    startPdi();
  }

  private synchronized void startPdi() throws IOException {
    pdiBuffer = pdiBufferSupplier.get();
    pdiAssembler.start(pdiBuffer);
  }

  @Override
  public synchronized void add(D domainObject) {
    try {
      Map<String, ContentInfo> contentInfo;
      contentInfo = contentAssembler.addContentsOf(domainObject);
      pdiAssembler.add(new HashedContents<>(domainObject, contentInfo));
      metrics.inc(SipMetrics.NUM_AIUS);
      setPdiSize(pdiBuffer.length()); // Approximate PDI size until the end, when we know for sure
    } catch (IOException e) {
      throw new RuntimeIoException(e);
    }
  }

  private void setPdiSize(long pdiSize) {
    metrics.set(SipMetrics.SIZE_PDI, pdiSize);
    metrics.set(SipMetrics.SIZE_SIP, metrics.get(SipMetrics.SIZE_DIGITAL_OBJECTS) + metrics.get(SipMetrics.SIZE_PDI));
  }

  @Override
  public synchronized void end() throws IOException {
    try {
      endPdi();
      addPackagingInformation();
    } finally {
      IOStreams.close(zip);
      metrics.set(SipMetrics.ASSEMBLY_TIME, System.currentTimeMillis() - metrics.get(SipMetrics.ASSEMBLY_TIME));
      metrics.set(SipMetrics.SIZE_SIP_FILE, sipFileBuffer.length());
    }
  }

  private synchronized void endPdi() throws IOException {
    try {
      pdiAssembler.end();
      addPdiToZip();
    } finally {
      pdiBuffer = null;
    }
  }

  void addPdiToZip() throws IOException {
    try (InputStream in = pdiBuffer.openForReading()) {
      pdiHash = zip.addEntry(PDI_ENTRY, in, pdiHashAssembler)
        .stream()
        .limit(1)
        .findAny();
    }
    setPdiSize(pdiHashAssembler.numBytesHashed());
  }

  private void addPackagingInformation() throws IOException {
    DataBuffer buffer = new MemoryBuffer();
    packagingInformationAssembler.start(buffer);
    packagingInformationAssembler.add(packagingInformation());
    packagingInformationAssembler.end();
    long packagingInformationSize = buffer.length();
    try (InputStream stream = buffer.openForReading()) {
      zip.addEntry(PACKAGING_INFORMATION_ENTRY, stream, new NoHashAssembler());
    }
    metrics.set(SipMetrics.SIZE_SIP,
        metrics.get(SipMetrics.SIZE_DIGITAL_OBJECTS) + metrics.get(SipMetrics.SIZE_PDI) + packagingInformationSize);
  }

  private PackagingInformation packagingInformation() {
    return packagingInformationFactory.newInstance(metrics.get(SipMetrics.NUM_AIUS), pdiHash);
  }

  @Override
  public SipMetrics getMetrics() {
    return new SipMetrics(metrics.forReading());
  }

  public PackagingInformationFactory getPackagingInformationFactory() {
    return packagingInformationFactory;
  }

}


/*
 * Copyright (c) 2016-2017 by OpenText Corporation. All Rights Reserved.
 */
package com.opentext.ia.sdk.support.io;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Objects;

/**
 * Utility methods to work with I/O streams.
 */
public final class IOStreams {

  private IOStreams() {
    // Utility class
  }

  /**
   * Utility method to copy the bytes from an InputStream to an OutputStream while also assembling a hash value in the
   * process.
   * @param in The source stream
   * @param out The target stream
   * @param bufferSize The size of the internal buffer
   * @param hashAssembler The HashAssembler to use.
   * @throws IOException if an error occurs when copying the streams
   */
  public static void copy(InputStream in, OutputStream out, int bufferSize, HashAssembler hashAssembler)
      throws IOException {
    byte[] buffer = new byte[bufferSize];
    int numRead = Objects.requireNonNull(in, "Missing input").read(buffer);
    if (numRead == 0) {
      throw new IllegalArgumentException("Missing content");
    }
    Objects.requireNonNull(out, "Missing output");
    while (numRead > 0) {
      out.write(buffer, 0, numRead);
      hashAssembler.add(buffer, numRead);
      numRead = in.read(buffer);
    }
  }

  public static void close(Closeable... closeables) {
    for (Closeable closeable : closeables) {
      if (closeable == null) {
        continue;
      }
      try {
        closeable.close();
      } catch (IOException ignored) {
        // Ignore
      }
    }
  }

}


/*
 * Copyright (c) 2016-2017 by OpenText Corporation. All Rights Reserved.
 */
package com.opentext.ia.sdk.support.xml;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.HashMap;
import java.util.Objects;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import javax.validation.ValidationException;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import org.apache.commons.lang3.StringUtils;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import com.opentext.ia.sdk.support.io.IOStreams;

/**
 * Generic XML processing functions.
 */
@SuppressWarnings({ "PMD.CyclomaticComplexity", "PMD.StdCyclomaticComplexity" })
public final class XmlUtil {

  private static final String NL = System.getProperty("line.separator");

  /**
   * The XML declaration to be used in the
   * prolog of an XML document.
   */
  public static final String XML_DECLARATION = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + NL;

  private XmlUtil() {
    // Utility class
  }

  private static final DocumentBuilderFactory VALIDATING_DOCUMENT_BUILDER_FACTORY = newSecureDocumentBuilderFactory();
  private static final ThreadLocal<DocumentBuilder> VALIDATING_DOCUMENT_BUILDER = ThreadLocal.withInitial(() -> {
    try {
      return VALIDATING_DOCUMENT_BUILDER_FACTORY.newDocumentBuilder();
    } catch (ParserConfigurationException e) {
      throw new IllegalStateException(e);
    }
  });
  private static final String INDENT = "  ";

  /**
   * Return a new factory for building XML documents that is configured to operate securely. The factory also supports
   * XML namespaces and validation.
   * @return The newly created factory
   */
  public static DocumentBuilderFactory newSecureDocumentBuilderFactory() {
    try {
      DocumentBuilderFactory result = DocumentBuilderFactory.newInstance();
      result.setFeature("http://javax.xml.XMLConstants/feature/secure-processing", true);
      result.setFeature("http://xml.org/sax/features/external-general-entities", false);
      result.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
      result.setNamespaceAware(true);
      result.setValidating(true);
      return result;
    } catch (ParserConfigurationException e) {
      throw new IllegalStateException(e);
    }
  }

  /**
   * Return a new empty XML document.
   * @return The newly created document
   */
  public static Document newDocument() {
    DocumentBuilder documentBuilder = getDocumentBuilder();
    try {
      return documentBuilder.newDocument();
    } finally {
      documentBuilder.reset();
    }
  }

  private static DocumentBuilder getDocumentBuilder() {
    DocumentBuilder result = VALIDATING_DOCUMENT_BUILDER.get();
    result.setErrorHandler(new DefaultErrorHandler());
    return result;
  }

  /**
   * Convert a given node to a pretty-printed string.
   * @param node The node to convert
   * @return The pretty-printed string
   */
  public static String toString(Node node) {
    return toString(node, "");
  }

  public static String toString(Node node, String indentation) {
    return toString(node, indentation, true);
  }

  public static String toString(Node node, boolean writeRootNamespace) {
    return toString(node, "", writeRootNamespace);
  }

  public static String toString(Node node, String indentation, boolean writeRootNamespace) {
    StringBuilder result = new StringBuilder();
    if (node != null) {
      append(node, indentation, new Namespaces(writeRootNamespace), result);
    }
    return result.toString();
  }

  @SuppressWarnings("PMD.CyclomaticComplexity")
  private static void append(Node node, String indentation, Namespaces namespaces, StringBuilder builder) {
    switch (node.getNodeType()) {
      case Node.ELEMENT_NODE:
        appendElement(node, indentation, namespaces, builder);
        break;
      case Node.ATTRIBUTE_NODE:
        appendAttribute(node, namespaces, builder);
        break;
      case Node.TEXT_NODE:
        appendText(node, builder);
        break;
      case Node.CDATA_SECTION_NODE:
        appendCdataSection(node, indentation, builder);
        break;
      case Node.ENTITY_NODE:
        throw new IllegalStateException("Unhandled node of type Entity");
      case Node.PROCESSING_INSTRUCTION_NODE:
        appendProcessingInstruction(node, indentation, builder);
        break;
      case Node.COMMENT_NODE:
        appendComment(node, indentation, builder);
        break;
      case Node.DOCUMENT_NODE:
        appendDocument(node, indentation, namespaces, builder);
        break;
      default:
        throw new UnsupportedOperationException("Unhandled node type: " + node.getNodeType());
    }
  }

  private static void appendDocument(Node node, String indentation, Namespaces namespaces,
      StringBuilder builder) {
    builder.append(XML_DECLARATION);
    for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
      append(child, indentation, namespaces, builder);
    }
  }

  private static void appendElement(Node node, String indentation, Namespaces namespaces,
      StringBuilder builder) {
    Node previousSibling = node.getPreviousSibling();
    if (previousSibling != null && previousSibling.getNodeType() == Node.TEXT_NODE
        && StringUtils.isNotBlank(previousSibling.getNodeValue())) {
      builder.append(NL);
    }
    Element element = (Element)node;
    openElement(indentation, builder, namespaces, element);
    appendAttributes(indentation, element, namespaces, builder);
    if (node.getFirstChild() == null) {
      builder.append("/>").append(NL);
    } else if (startsWithNonWhitespaceText(node)) {
      builder.append('>');
      appendChildren(node, indentation, namespaces, builder);
      closeElement(builder, element);
    } else {
      builder.append('>').append(NL);
      appendChildren(node, indentation, namespaces, builder);
      builder.append(indentation);
      closeElement(builder, element);
    }
  }

  private static StringBuilder openElement(String indentation, StringBuilder builder, Namespaces namespaces,
      Element element) {
    String tag = element.getTagName();
    builder.append(indentation).append('<').append(tag);
    if (hasDifferentNamespaceThanParent(element, namespaces)) {
      int index = tag.indexOf(':');
      if (index < 0) {
        builder.append(" xmlns=\"")
          .append(element.getNamespaceURI())
          .append('"');
      } else {
        builder.append(" xmlns:")
          .append(tag.substring(0, index))
          .append("=\"")
          .append(element.getNamespaceURI())
          .append('"');
      }
    }
    return builder;
  }

  private static boolean hasDifferentNamespaceThanParent(Node node, Namespaces namespaces) {
    if (node.getNamespaceURI() == null || isNamespaceNode(node)) {
      return false;
    }
    Node parent;
    if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
      parent = ((Attr)node).getOwnerElement();
    } else {
      parent = node.getParentNode();
    }
    if (parent == null) {
      return true;
    }
    if (parent instanceof Document) {
      return namespaces.isIncludeRootNamespace();
    }
    return !node.getNamespaceURI().equals(parent.getNamespaceURI());
  }

  private static boolean isNamespaceNode(Node node) {
    return node.getNodeType() == Node.ATTRIBUTE_NODE && node.getNodeName()
      .startsWith("xmlns") && "http://www.w3.org/2000/xmlns/".equals(node.getNamespaceURI());
  }

  private static void appendAttributes(String indentation, Element element, Namespaces namespaces,
      StringBuilder builder) {
    NamedNodeMap attributes = element.getAttributes();
    for (int i = 0; i < attributes.getLength(); i++) {
      append(attributes.item(i), indentation + INDENT, namespaces, builder);
    }
  }

  private static boolean startsWithNonWhitespaceText(Node node) {
    StringBuilder text = new StringBuilder();
    Node child = node.getFirstChild();
    while (child != null) {
      if (child.getNodeType() != Node.TEXT_NODE) {
        break;
      }
      text.append(child.getNodeValue());
      child = child.getNextSibling();
    }
    return StringUtils.isNotBlank(text);
  }

  private static void appendChildren(Node node, String indentation, Namespaces namespaces,
      StringBuilder builder) {
    for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
      if (child.getNodeType() != Node.ATTRIBUTE_NODE) {
        append(child, indentation + INDENT, namespaces, builder);
      }
    }
  }

  private static StringBuilder closeElement(StringBuilder builder, Element element) {
    return builder.append("</")
      .append(element.getTagName())
      .append('>').append(NL);
  }

  private static void appendAttribute(Node node, Namespaces namespaces, StringBuilder builder) {
    if (isNamespaceNode(node)) {
      return;
    }
    builder.append(' ');
    if (hasDifferentNamespaceThanParent(node, namespaces)) {
      String uri = node.getNamespaceURI();
      Prefix prefix = getPrefix(uri, namespaces);
      if (!prefix.existing && !namespaces.isXmlNamespace(uri)) {
        builder.append("xmlns:")
          .append(prefix)
          .append("=\"")
          .append(uri)
          .append("\" ");
      }
      builder.append(prefix).append(':');
    }
    builder.append(getAttributeName(node)).append("=\"").append(XmlUtil.escape(valueOf(node))).append('\"');
  }

  private static String getAttributeName(Node node) {
    String result = node.getLocalName();
    if (result == null) {
      // No namespace support while parsing
      result = node.getNodeName();
    }
    return result;
  }

  private static Prefix getPrefix(String uri, Namespaces namespaces) {
    String result = namespaces.get(uri);
    boolean existing = result != null;
    if (!existing) {
      result = namespaces.add(uri);
    }
    return new Prefix(result, existing);
  }

  private static void appendText(Node node, StringBuilder builder) {
    builder.append(valueOf(node));
  }

  private static String valueOf(Node node) {
    return node.getNodeValue().trim();
  }

  private static void appendComment(Node node, String indentation, StringBuilder builder) {
    builder.append(indentation)
      .append("").append(NL);
  }

  private static void appendProcessingInstruction(Node node, String indentation, StringBuilder builder) {
    builder.append(indentation)
      .append("<?")
      .append(node.getNodeName())
      .append(' ')
      .append(valueOf(node))
      .append("?>").append(NL);
  }

  private static void appendCdataSection(Node node, String indentation, StringBuilder builder) {
    builder.append(indentation)
      .append("<![CDATA[")
      .append(valueOf(node))
      .append("]]>").append(NL);
  }

  /**
   * Parse the content of a given file into an XML document.
   * @param file The file to parse
   * @return The parsed XML document
   */
  public static Document parse(File file) {
    if (!file.isFile()) {
      throw new IllegalArgumentException("Missing file: " + file.getAbsolutePath());
    }
    try {
      try (InputStream stream = Files.newInputStream(file.toPath(), StandardOpenOption.READ)) {
        return parse(stream);
      }
    } catch (IOException e) {
      throw new IllegalArgumentException("Failed to parse " + file.getAbsolutePath(), e);
    }
  }

  /**
   * Parse the content of a given input stream into an XML document.
   * @param stream The input stream to parse
   * @return The parsed XML document
   */
  public static Document parse(InputStream stream) {
    return parse(new InputStreamReader(stream, StandardCharsets.UTF_8));
  }

  /**
   * Parse the content of a given reader into an XML document.
   * @param reader The reader to parse
   * @return The parsed XML document
   */
  public static Document parse(Reader reader) {
    DocumentBuilder documentBuilder = getDocumentBuilder();
    try {
      return documentBuilder.parse(new InputSource(reader));
    } catch (SAXException | IOException e) {
      throw new IllegalArgumentException("Failed to parse XML document", e);
    } finally {
      documentBuilder.reset();
    }
  }

  /**
   * Return the elements under a given parent element.
   * @param parent The parent of the elements to return
   * @return The children of the parent element
   */
  public static Stream<Element> elementsIn(Element parent) {
    return nodesIn(parent).filter(n -> n.getNodeType() == Node.ELEMENT_NODE)
      .map(n -> (Element)n);
  }

  /**
   * Return the nodes under a given parent element.
   * @param parent The parent of the nodes to return
   * @return The children of the parent element
   */
  public static Stream<Node> nodesIn(Element parent) {
    return StreamSupport.stream(new ChildNodesSpliterator(parent), false);
  }

  /**
   * Return the first child element of a given parent element whose tag matches any of the given names. When no child
   * names are given, the first child element is returned.
   * @param parent The parent of the element to return
   * @param childNames The acceptable names of the children
   * @return The first child element that matches any of the names
   */
  public static Element getFirstChildElement(Element parent, String... childNames) {
    return firstOf(namedElementsIn(parent, childNames));
  }

  /**
   * Return the elements under a given parent element whose tag matches any of the given names. When no child
   * names are given, all child elements are returned.
   * @param parent The parent of the elements to return
   * @param childNames The acceptable names of the children
   * @return The children of the parent element
   */
  public static Stream<Element> namedElementsIn(Element parent, String... childNames) {
    return elementsIn(parent).filter(e -> isName(e, childNames));
  }

  private static boolean isName(Element element, String... names) {
    if (names == null || names.length == 0) {
      return true;
    }
    if (names.length == 1 && names[0] == null) {
      return true;
    }
    String lookingFor = element.getLocalName();
    for (String name : names) {
      if (lookingFor.equals(name)) {
        return true;
      }
    }
    return false;
  }

  private static Element firstOf(Stream<Element> elements) {
    return elements.findFirst().orElse(null);
  }

  /**
   * Validate an XML document against an XML Schema document.
   * @param xml The XML document to validate
   * @param xmlSchema The XML Schema document to validate against
   * @param humanFriendlyDocumentType A human-friendly name that describes the schema
   * @throws IOException When an I/O error occurs
   */
  public static void validate(InputStream xml, InputStream xmlSchema, String humanFriendlyDocumentType)
      throws IOException {
    try {
      newXmlSchemaValidator(xmlSchema).validate(new StreamSource(Objects.requireNonNull(xml)));
    } catch (SAXException e) {
      throw new ValidationException("Invalid " + humanFriendlyDocumentType, e);
    }
  }

  @SuppressWarnings({ "PMD.AvoidCatchingNPE", "PMD.AvoidCatchingGenericException" }) // Want better error message
  public static Validator newXmlSchemaValidator(InputStream xmlSchema) {
    try {
      return SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)
        .newSchema(new StreamSource(xmlSchema))
        .newValidator();
    } catch (SAXException | NullPointerException e) {
      throw new ValidationException("Invalid XML Schema", e);
    } finally {
      IOStreams.close(xmlSchema);
    }
  }

  private static final class DefaultErrorHandler implements ErrorHandler {

    @Override
    public void warning(SAXParseException exception) throws SAXException {
      // Do nothing
    }

    @Override
    public void fatalError(SAXParseException exception) throws SAXException {
      throw exception;
    }

    @Override
    public void error(SAXParseException exception) throws SAXException {
      if (!isMissingSchemaError(exception)) {
        throw exception;
      }
    }

    private boolean isMissingSchemaError(SAXParseException exception) {
      String message = exception.getMessage();
      if (message == null) {
        return false;
      }
      return message.contains("no grammar found") || message.contains("must match DOCTYPE root \"null\"");
    }
  }

  public static String escape(String text) {
    return escapeInvalidUnicodeChars(text.replace("&", "&")
      .replace("<", "<")
      .replace(">", ">")
      .replace("'", "'")
      .replace("\"", """)
      .replaceAll("[\u0001-\u0008\u000b-\u000c\u000e-\u001f\ud800-\udfff\ufffe-\uffff]", ""));
  }

  private static String escapeInvalidUnicodeChars(String text) {
    StringBuilder result = new StringBuilder(text);
    int index = 0;
    for (int i = 0; i < text.length(); i++) {
      int value = Character.codePointAt(text, i);
      if (value < 32 && value != 10 && value != 13) {
        String unicodeEntity = String.format("&#%04x;", value);
        result.replace(index, index + 1, unicodeEntity);
        index += unicodeEntity.length();
      } else {
        index++;
      }
    }
    return result.toString();
  }

  private static class Namespaces extends HashMap<String, String> {

    private static final long serialVersionUID = -2861002177169934851L;
    private static final String XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace";

    private final boolean includeRootNamespace;
    private int counter;

    Namespaces(boolean includeRootNamespace) {
      this.includeRootNamespace = includeRootNamespace;
      put("http://www.w3.org/2001/XMLSchema-instance", "xsi");
      put(XML_NAMESPACE, "xml");
    }

    public String add(String uri) {
      String result = "ns" + ++counter;
      put(uri, result);
      return result;
    }

    public boolean isIncludeRootNamespace() {
      return includeRootNamespace;
    }

    public boolean isXmlNamespace(String uri) {
      return XML_NAMESPACE.equals(uri);
    }
  }

  private static class Prefix {

    private final String name;
    private final boolean existing;

    Prefix(String name, boolean existing) {
      this.name = name;
      this.existing = existing;
    }

    @Override
    public String toString() {
      return name;
    }

  }

}


/*
 * Copyright (c) 2016-2017 by OpenText Corporation. All Rights Reserved.
 */
package com.opentext.ia.sdk.client.factory;

import java.io.IOException;
import java.util.Objects;

import com.opentext.ia.sdk.client.api.ArchiveClient;
import com.opentext.ia.sdk.client.api.ArchiveConnection;
import com.opentext.ia.sdk.client.api.InfoArchiveLinkRelations;
import com.opentext.ia.sdk.client.impl.ApplicationIngestionResourcesCache;
import com.opentext.ia.sdk.client.impl.InfoArchiveRestClient;
import com.opentext.ia.sdk.dto.Application;
import com.opentext.ia.sdk.dto.Applications;
import com.opentext.ia.sdk.dto.ProductInfo;
import com.opentext.ia.sdk.dto.Services;
import com.opentext.ia.sdk.dto.Tenant;
import com.opentext.ia.sdk.server.configuration.ApplicationConfigurer;
import com.opentext.ia.sdk.support.http.rest.LinkContainer;
import com.opentext.ia.sdk.support.http.rest.RestClient;

/**
 * Factory methods for creating {@linkplain ArchiveClient}s.
 */
public final class ArchiveClients {

  private ArchiveClients() {
    // Utility class
  }

  /**
   * Returns an {@linkplain ArchiveClient} for an application that it first configures.
   * @param configurer How to configure the InfoArchive application
   * @param connection How to communicate with the InfoArchive server
   * @return An ArchiveClient
   * @throws IOException When an I/O error occurs
   */
  public static ArchiveClient configuringApplicationUsing(ApplicationConfigurer configurer,
      ArchiveConnection connection) throws IOException {
    configurer.configure(connection);
    return usingAlreadyConfiguredApplication(configurer.getApplicationName(), connection);
  }

  /**
   * Returns an {@linkplain ArchiveClient} for an already configured application.
   * @param applicationName The name of the already configured application to use
   * @param connection How to communicate with the InfoArchive server
   * @return An ArchiveClient
   * @throws IOException When an I/O error occurs
   */
  public static ArchiveClient usingAlreadyConfiguredApplication(String applicationName, ArchiveConnection connection)
      throws IOException {
    RestClient restClient = connection.getRestClient();
    return new InfoArchiveRestClient(restClient, appResourceCache(applicationName, connection, restClient));
  }

  private static ApplicationIngestionResourcesCache appResourceCache(String applicationName,
      ArchiveConnection connection, RestClient restClient) throws IOException {
    ApplicationIngestionResourcesCache resourceCache = new ApplicationIngestionResourcesCache(applicationName);
    Services services = connection.getServices();
    ProductInfo productInfo = getProductInfo(restClient, services);
    Tenant tenant = getTenant(restClient, services);
    Application application = getApplication(restClient, tenant, applicationName);
    cacheResourceUris(restClient, productInfo, application, resourceCache);
    return resourceCache;
  }

  private static ProductInfo getProductInfo(RestClient restClient, Services services) throws IOException {
    return Objects.requireNonNull(restClient.follow(services, InfoArchiveLinkRelations.LINK_PRODUCT_INFO,
        ProductInfo.class), "Product Info not found.");
  }

  private static Tenant getTenant(RestClient restClient, Services services) throws IOException {
    return Objects.requireNonNull(restClient.follow(services, InfoArchiveLinkRelations.LINK_TENANT, Tenant.class),
        "Tenant not found.");
  }

  private static Application getApplication(RestClient restClient, Tenant tenant, String applicationName)
      throws IOException {
    Applications applications = restClient.followNonPaged(tenant, InfoArchiveLinkRelations.LINK_APPLICATIONS,
        Applications.class);
    return Objects.requireNonNull(applications.byName(applicationName),
        "Application " + applicationName + " not found.");
  }

  private static void cacheResourceUris(RestClient restClient, ProductInfo productInfo, Application application,
      ApplicationIngestionResourcesCache resourceCache) throws IOException {
    resourceCache.setServerVersion(productInfo.getBuildProperties().getServerVersion());

    resourceCache.setCiResourceUri(application.getUri(InfoArchiveLinkRelations.LINK_CI));
    resourceCache.setAipResourceUri(application.getUri(InfoArchiveLinkRelations.LINK_AIPS));
    LinkContainer aips = restClient.follow(application, InfoArchiveLinkRelations.LINK_AIPS, LinkContainer.class);
    resourceCache.setAipIngestDirectResourceUri(aips.getUri(InfoArchiveLinkRelations.LINK_INGEST_DIRECT));
  }

}


/*
 * Copyright (c) 2016-2017 by OpenText Corporation. All Rights Reserved.
 */
package com.opentext.ia.yaml.core;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import org.apache.commons.io.IOUtils;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.error.MarkedYAMLException;
import org.yaml.snakeyaml.representer.Representer;

/**
 * Type-safe access to a YAML map.
 */
@SuppressWarnings("unchecked")
public class YamlMap {

  private static final String LINE_SEPARATOR = System.lineSeparator();

  private static final int MAX_LINE_LENGTH = 80;

  private static YamlSyntaxErrorPrettifier yamlSyntaxErrorPrettifier = new YamlSyntaxErrorPrettifier();

  private Map<String, Object> data;

  public static YamlMap from(YamlMap source) {
    if (source == null) {
      throw new IllegalArgumentException("Missing YAML");
    }
    return from(source.toString());
  }

  /**
   * Parses the given YAML.
   * @param yaml The YAML string to parse
   * @return A YamlMap corresponding to the provided YAML string
   */
  public static YamlMap from(String yaml) {
    assertNotNull(yaml);
    try (InputStream input = streamOf(yaml)) {
      return from(input);
    } catch (IOException e) {
      throw new IllegalArgumentException("Failed to parse YAML string", e);
    }
  }

  private static void assertNotNull(Object yaml) {
    if (yaml == null) {
      throw new IllegalArgumentException("Missing YAML");
    }
  }

  private static InputStream streamOf(String text) {
    return IOUtils.toInputStream(text, StandardCharsets.UTF_8);
  }

  /**
   * Parses the given YAML.
   * @param yaml The YAML stream to parse
   * @return A YamlMap corresponding to the provided YAML input stream
   */
  public static YamlMap from(InputStream yaml) {
    return from(yaml, yaml instanceof ByteArrayInputStream);
  }

  public static YamlMap from(InputStream yaml, boolean canReset) {
    assertNotNull(yaml);
    InputStream input = canReset ? yaml : inMemoryCopyOf(yaml);
    YamlMap result = new YamlMap();
    try {
      for (Object data : newLoader().loadAll(input)) {
        result.putAll(new YamlMap(data));
      }
    } catch (MarkedYAMLException e) {
      try {
        input.reset();
        throw yamlSyntaxErrorPrettifier.apply(e, IOUtils.toString(input, StandardCharsets.UTF_8));
      } catch (IOException ignored) {
        // NOTREACHED
      }
    }
    return result;
  }

  private static InputStream inMemoryCopyOf(InputStream input) {
    try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
      IOUtils.copy(input, output);
      return new ByteArrayInputStream(output.toByteArray());
    } catch (IOException e) {
      throw new IllegalStateException(e);
    }
  }

  private static Yaml newLoader() {
    return new Yaml(new Constructor(), new Representer(), new DumperOptions(), new YamlTypeResolver());
  }

  /**
   * Parses the given YAML.
   * @param yaml The YAML file to parse
   * @return A YamlMap corresponding to the provided YAML file, or an empty YamlMap if the
   * file does not exist
   * @throws IOException When an I/O error occurs
   */
  public static YamlMap from(File yaml) throws IOException {
    assertNotNull(yaml);
    if (!yaml.isFile()) {
      return new YamlMap();
    }
    try (InputStream input = Files.newInputStream(yaml.toPath())) {
      return from(input);
    }
  }

  public YamlMap() {
    this(null);
  }

  public YamlMap(Object data) {
    this.data = data instanceof Map ? (Map<String, Object>)data : new LinkedHashMap<>();
  }

  public boolean isEmpty() {
    return data.isEmpty();
  }

  public int size() {
    return data.size();
  }

  public YamlMap put(String key, Object value) {
    data.put(key, unpack(value));
    return this;
  }

  @SuppressWarnings({ "unchecked", "rawtypes" })
  private Object unpack(Object value) {
    Object result = value;
    if (result instanceof YamlMap) {
      result = ((YamlMap)result).getRawData();
    }
    if (result instanceof Iterable) {
      Collection<Object> values = new ArrayList<>();
      ((Iterable)result).forEach(v -> values.add(unpack(v)));
      return values;
    }
    if (result instanceof Value) {
      result = ((Value)result).getRawData();
    }
    if (result instanceof String) {
      result = normalizeWhitespace((String)result);
    }
    return result;
  }

  private String normalizeWhitespace(String text) {
    StringBuilder result = new StringBuilder();
    String prefix = "";
    for (String line : text.split("(\n|\r)+")) {
      int end = line.length() - 1;
      while (end >= 0 && Character.isWhitespace(line.charAt(end))) {
        --end;
      }
      result.append(prefix);
      if (end >= 0) {
        result.append(line.substring(0, end + 1));
      }
      prefix = LINE_SEPARATOR;
    }
    return result.toString();
  }

  Map<String, Object> getRawData() {
    return data;
  }

  public void putAll(YamlMap other) {
    data.putAll(other.data);
  }

  public boolean containsKey(String key) {
    return data.containsKey(key);
  }

  public YamlMap remove(String key) {
    data.remove(key);
    return this;
  }

  public Value get(Object... keys) {
    YamlMap map = this;
    int i = 0;
    while (i < keys.length - 1) {
      Value value = map.get(keys[i++]);
      if (value.isList()) {
        int index = toListIndex(keys[i++]);
        value = value.toList().get(index);
        if (i < keys.length) {
          map = value.toMap();
        } else {
          return value;
        }
      } else {
        map = value.toMap();
      }
    }
    return new Value(map.data.get(keys[keys.length - 1]));
  }

  private int toListIndex(Object index) {
    if (!Integer.class.isAssignableFrom(Objects.requireNonNull(index).getClass())) {
      throw new IllegalArgumentException("Expecting an index in a sequence, but got: " + index);
    }
    return (int)index;
  }

  public Stream<Value> values() {
    return entries().map(Entry::getValue);
  }

  public Stream<Entry> entries() {
    return data.entrySet().stream()
        .map(entry -> new Entry(this, entry.getKey(), new Value(entry.getValue())))
        .sorted((a, b) -> new DefaultYamlComparator().compare(a.getKey(), b.getKey()));
  }

  public void visit(Visitor visitor) {
    visit(visitor, new Visit(this));
  }

  private void visit(Visitor visitor, Visit visit) {
    boolean shouldVisit = visitor.test(visit);
    if (shouldVisit) {
      visitor.accept(visit);
    }
    if (visitor.maxNesting() > visit.getLevel()) {
      visit.getMap().entries().forEach(entry -> {
        String key = entry.getKey();
        Value value = entry.getValue();
        if (value.isMap()) {
          visit(visitor, visit.descend(key));
        } else if (isListOfMaps(value)) {
          IntStream.range(0, value.toList().size())
              .forEach(index -> visit(visitor, visit.descend(key, index)));
        }
      });
    }
    if (shouldVisit) {
      visitor.afterVisit(visit);
    }
  }

  private boolean isListOfMaps(Value value) {
    List<Value> values = value.toList();
    if (values.isEmpty()) {
      return false;
    }
    return values.size() == values.stream()
        .filter(Value::isMap)
        .count();
  }

  public InputStream toStream() {
    return streamOf(toString());
  }

  public YamlMap sort() {
    return sort(new DefaultYamlComparator());
  }

  public YamlMap sort(Comparator<String> comparator) {
    return sort(comparator, true);
  }

  public YamlMap sort(boolean recursive) {
    return sort(new DefaultYamlComparator(), recursive);
  }

  public YamlMap sort(Comparator<String> comparator, boolean recursive) {
    return sort(comparator, ignored -> true, recursive);
  }

  public YamlMap sort(Predicate<String> entriesFilter) {
    return sort(new DefaultYamlComparator(), entriesFilter, true);
  }

  public YamlMap sort(Comparator<String> comparator, Predicate<String> entriesFilter) {
    return sort(comparator, entriesFilter, true);
  }

  public YamlMap sort(Comparator<String> comparator, Predicate<String> entriesFilter, boolean recursive) {
    data = sortMap(data, comparator);
    if (recursive) {
      sortRecursively(data, comparator, entriesFilter);
    }
    return this;
  }

  private Map<String, Object> sortMap(Map<String, Object> source, Comparator<String> comparator) {
    Map<String, Object> result = new TreeMap<>(comparator);
    source.entrySet().forEach(entry -> result.put(entry.getKey(), entry.getValue()));
    if (source instanceof LinkedHashMap) {
      source.clear();
      source.putAll(result);
      return source;
    }
    return result;
  }

  private void sortRecursively(Map<String, Object> map, Comparator<String> comparator,
      Predicate<String> entriesFilter) {
    map.keySet().stream()
        .filter(entriesFilter)
        .forEach(key -> {
          Object value = map.get(key);
          if (value instanceof Map) {
            sortMap(map, comparator, entriesFilter, key, (Map<String, Object>)value);
          } else if (value instanceof List) {
            sortList(map, comparator, entriesFilter, key, (List<?>)value);
          }
        });
  }

  private void sortMap(Map<String, Object> map, Comparator<String> comparator, Predicate<String> entriesFilter,
      String key, Map<String, Object> value) {
    Map<String, Object> sortedMap = sortMap(value, comparator);
    sortRecursively(sortedMap, comparator, entriesFilter);
    map.put(key, sortedMap);
  }

  private void sortList(Map<String, Object> map, Comparator<String> comparator, Predicate<String> entriesFilter,
      String key, List<?> list) {
    List<Object> sortedList = new ArrayList<>(list);
    ListContent content = new ListContent();
    for (int i = 0; i < sortedList.size(); i++) {
      sortListItem(sortedList, i, comparator, entriesFilter, content);
    }
    if (content.isAllMaps()) {
      Collections.sort(sortedList, new DefaultYamlSequenceComparator());
    }
    map.put(key, sortedList);
  }

  private void sortListItem(List<Object> sortedList, int index, Comparator<String> comparator,
      Predicate<String> entriesFilter, ListContent content) {
    Object value = sortedList.get(index);
    if (value instanceof Map) {
      Map<String, Object> sortedMap = sortMap((Map<String, Object>)value, comparator);
      sortRecursively(sortedMap, comparator, entriesFilter);
      sortedList.set(index, sortedMap);
      content.setAllScalars(false);
    } else {
      content.setAllMaps(false);
    }
    if (content.isAllScalars()) {
      Collections.sort(sortedList, (a, b) -> String.valueOf(a).compareTo(String.valueOf(b)));
    }
  }

  public YamlMap replace(String key, Object newValue) {
    return put(key, newValue);
  }

  public YamlMap replace(String oldKey, String newKey, Object newValue) {
    Map<String, Object> newEntries = new LinkedHashMap<>();
    for (Map.Entry<String, Object> entry : data.entrySet()) {
      if (oldKey.equals(entry.getKey())) {
        newEntries.put(newKey, unpack(newValue));
      } else {
        newEntries.put(entry.getKey(), entry.getValue());
      }
    }
    data.clear();
    data.putAll(newEntries);
    return this;
  }

  @Override
  public String toString() {
    StringBuilder result = new StringBuilder();
    appendMap(new YamlIndent(), hasMultipleNestedEntries(), data, result, new AtomicBoolean());
    return result.toString();
  }

  private boolean hasMultipleNestedEntries() {
    return size() > 1 && entries()
        .map(Entry::getValue)
        .anyMatch(value -> value.isMap() || value.isList());
  }

  private void appendMap(YamlIndent indent, boolean separateEntries, Map<String, Object> map, StringBuilder builder,
      AtomicBoolean differenceFound) {
    boolean hasOutput = false;
    for (Map.Entry<String, Object> entry : map.entrySet()) {
      if (hasOutput && separateEntries) {
        builder.append(LINE_SEPARATOR);
      }
      appendEntry(indent, entry, builder, differenceFound);
      hasOutput = true;
    }
    if (!hasOutput) {
      builder.append(indent).append("{ }").append(LINE_SEPARATOR);
    }
  }

  private void appendEntry(YamlIndent indent, Map.Entry<String, Object> entry, StringBuilder builder,
      AtomicBoolean differenceFound) {
    int len = builder.length();
    builder.append(indent);
    appendText(indent, builder.length() - len, entry.getKey(), builder);
    builder.append(':');
    Object value = entry.getValue();
    if (value instanceof Map) {
      builder.append(LINE_SEPARATOR);
      appendMap(indent.inMap(), false, (Map<String, Object>)value, builder, differenceFound);
    } else if (value instanceof Collection) {
      appendCollection(indent, builder.length() - len, (Collection<?>)value, builder, differenceFound);
    } else {
      builder.append(' ');
      appendValue(indent, builder.length() - len, value, builder);
    }
  }

  private void appendText(YamlIndent indent, int currentLineLength, String text, StringBuilder builder) {
    if (needsToBeSingleQuoted(text)) {
      appendSingleQuoted(text, builder);
    } else if (needsToBeDoubleQuoted(text)) {
      appendDoubleQuoted(text, builder);
    } else if (MAX_LINE_LENGTH < currentLineLength + text.length()) {
      appendWrappedText(indent.inText(), currentLineLength, text, builder);
    } else {
      builder.append(text);
    }
  }

  private boolean needsToBeSingleQuoted(String text) {
    return text.isEmpty() || text.matches("([\"%@*,].*)|(.*#.*)|(.*:\\s.*)|(^0\\d*$)");
  }

  private void appendSingleQuoted(String text, StringBuilder builder) {
    builder.append('\'').append(text.replace("'", "''")).append('\'');
  }

  private boolean needsToBeDoubleQuoted(String text) {
    return text.charAt(0) == '\'';
  }

  private void appendDoubleQuoted(String text, StringBuilder builder) {
    builder.append('"').append(text.replace("\"", "\\\"")).append('"');
  }

  private void appendWrappedText(YamlIndent indent, int currentLineLength, String text, StringBuilder builder) {
    int start = 0;
    int used = currentLineLength;
    int length = text.length();
    while (start < length) {
      int end = wordBreakAfter(text, start + MAX_LINE_LENGTH - used + 1);
      builder.append(text.substring(start, end));
      start = end + 1;
      while (start < length && Character.isWhitespace(text.charAt(start))) {
        start++;
      }
      if (start < length) {
        builder.append(LINE_SEPARATOR).append(indent);
        used = indent.length();
      }
    }
  }

  private int wordBreakAfter(String text, int start) {
    int result = text.indexOf(' ', start);
    if (result < 0) {
      result = text.length();
    }
    return result;
  }

  private void appendCollection(YamlIndent indent, int currentLineLength, Collection<?> collection,
      StringBuilder builder, AtomicBoolean differenceFound) {
    int len = builder.length();
    if (collection.isEmpty()) {
      builder.append(" [ ]").append(LINE_SEPARATOR);
    } else {
      builder.append(LINE_SEPARATOR);
      for (Object item : collection) {
        if (item instanceof Map) {
          appendMap(indent.inSequence(), false, (Map<String, Object>)item, builder, differenceFound);
        } else {
          builder.append(indent).append("- ");
          appendValue(indent, currentLineLength + builder.length() - len, item, builder);
        }
      }
    }
  }

  private void appendValue(YamlIndent indent, int currentLineLength, Object value, StringBuilder builder) {
    if (value instanceof String) {
      String text = ((String)value).trim().replace("\t", "  ");
      String[] lines = text.split("\\s*(\n|\r)+");
      if (lines.length > 1) {
        builder.append('|');
        for (String line : lines) {
          builder.append(LINE_SEPARATOR).append(indent).append("  ").append(line);
        }
      } else {
        appendText(indent, currentLineLength, text, builder);
      }
    } else {
      builder.append(value);
    }
    builder.append(LINE_SEPARATOR);
  }

  private static class ListContent {

    private boolean allScalars = true;
    private boolean allMaps = true;

    boolean isAllScalars() {
      return allScalars;
    }

    void setAllScalars(boolean allScalars) {
      this.allScalars = allScalars;
    }

    boolean isAllMaps() {
      return allMaps;
    }

    void setAllMaps(boolean allMaps) {
      this.allMaps = allMaps;
    }

  }

}


/*
 * Copyright (c) 2016-2017 by OpenText Corporation. All Rights Reserved.
 */
package com.opentext.ia.sdk.sample.assemble;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.util.Collections;
import java.util.Map;

import com.opentext.ia.sdk.sip.ContentInfo;
import com.opentext.ia.sdk.sip.DigitalObject;
import com.opentext.ia.sdk.sip.DigitalObjectsExtraction;
import com.opentext.ia.sdk.sip.PackagingInformation;
import com.opentext.ia.sdk.sip.PdiAssembler;
import com.opentext.ia.sdk.sip.SipAssembler;
import com.opentext.ia.sdk.sip.SipMetrics;
import com.opentext.ia.sdk.sip.XmlPdiAssembler;
import com.opentext.ia.sdk.support.io.Encoding;
import com.opentext.ia.sdk.support.io.FileBuffer;
import com.opentext.ia.sdk.support.io.HashAssembler;
import com.opentext.ia.sdk.support.io.HashFunction;
import com.opentext.ia.sdk.support.io.RuntimeIoException;
import com.opentext.ia.sdk.support.io.SingleHashAssembler;

@SuppressWarnings("PMD")
public class FileArchiver {

  /**
   * Build a SIP archive from all files in a given directory tree.
   * @param args The command line args:

   *
The path to the directory to be archived. The default value is the current directory.

   *
The path to the SIP archive to be built. The default value is build/files.zip

   *

   */
  public static void main(String[] args) {
    try {
      Arguments arguments = new Arguments(args);
      File root = new File(arguments.next("content"));
      if (!root.isDirectory()) {
        root = new File(".");
      }
      String rootPath = root.getCanonicalPath();
      String sip = arguments.next("build/files.zip");
      new FileArchiver().run(rootPath, sip);
    } catch (IOException e) {
      e.printStackTrace(System.out);
      System.exit(1);
    }
  }

  private void run(String rootPath, String sip) throws IOException {
    File sipFile = new File(sip).getCanonicalFile();
    if (!sipFile.getParentFile().mkdirs() && !sipFile.getParentFile().isDirectory()) {
      throw new IllegalStateException("Could not create all required directories in path "
          + sipFile.getParentFile().getAbsolutePath());
    }
    System.out.printf("%nSample 2: Archiving files from %s into %s%n", rootPath, sipFile.getPath());

    // Tell InfoArchive where and how to archive the data
    URI entityUri = URI.create("urn:com.opentext.ia.sdk.sample.file:1.0");
    String entityName = "file";
    PackagingInformation prototype =
        PackagingInformation.builder().dss().application("fileApplication").holding("fileHolding")
            .producer("SIP SDK").entity(entityName).schema(entityUri.toString()).end().build();

    // Define a mapping from our domain object to the PDI XML
    PdiAssembler<File> pdiAssembler = new XmlPdiAssembler<File>(entityUri, entityName) {

      @Override
      protected void doAdd(File file, Map<String, ContentInfo> contentInfo) {
        try {
          String path = relativePath(file, rootPath);
          getBuilder().element("path", path).element("size", Long.toString(file.length()))
              .element("permissions", permissionsOf(file))
              .element("contentType", Files.probeContentType(file.toPath())).elements("hashes",
                  "hash", contentInfo.get(path).getContentHashes(), (hash, builder) -> {
                    builder.attribute("algorithm", hash.getHashFunction())
                        .attribute("encoding", hash.getEncoding())
                        .attribute("value", hash.getValue());
                  });
        } catch (IOException e) {
          throw new RuntimeIoException(e);
        }
      }
    };
    DigitalObjectsExtraction<File> contentAssembler = file -> Collections
        .singleton(DigitalObject.fromFile(relativePath(file, rootPath), file)).iterator();
    HashAssembler contentHashAssembler =
        new SingleHashAssembler(HashFunction.SHA256, Encoding.BASE64);

    // Assemble the SIP
    SipAssembler<File> assembler = SipAssembler.forPdiAndContentWithContentHashing(prototype,
        pdiAssembler, contentAssembler, contentHashAssembler);
    assembler.start(new FileBuffer(sipFile));
    try {
      addFilesIn(new File(rootPath), rootPath, relativePath(sipFile, rootPath), assembler);
    } finally {
      assembler.end();
    }

    // Show metrics about the assembly process
    SipMetrics metrics = assembler.getMetrics();
    System.out.printf("  Added %d files to SIP of %d bytes in %d ms%n", metrics.numDigitalObjects(),
        metrics.sipFileSize(), metrics.assemblyTime());
  }

  private String relativePath(File file, String rootPath) {
    return file.getAbsolutePath().substring(rootPath.length() + 1);
  }

  private String permissionsOf(File file) {
    return String.format("%c%c%c", file.canRead() ? 'r' : '-', file.canWrite() ? 'w' : '-',
        file.canExecute() ? 'x' : '-');
  }

  private void addFilesIn(File parent, String rootPath, String skipPath,
      SipAssembler<File> assembler) {
    File[] children = parent.listFiles();
    if (children != null) {
      for (File child : children) {
        if (child.isDirectory()) {
          addFilesIn(child, rootPath, skipPath, assembler);
        } else if (child.isFile() && !relativePath(child, rootPath).equals(skipPath)) {
          assembler.add(child);
        }
      }
    }
  }

  public static class Arguments {

    private final String[] args;
    private int index;

    public Arguments(String... args) {
      this.args = args;
    }

    public String next(String defaultValue) {
      return index < args.length ? args[index++] : defaultValue;
    }

  }

}


/*
 * Copyright (c) 2016-2017 by OpenText Corporation. All Rights Reserved.
 */
package com.opentext.ia.configuration;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Objects;

import org.apache.commons.io.IOUtils;

/**
 * Build a content object.
 * @author Ray Sinnema
 * @since 9.13.0
 */
public class ContentBuilder<P extends ContentObjectBuilder<?, P, ?>> {

  private final ContentObjectBuilder<?, P, ?> parent;
  private String text;
  private String format;

  protected ContentBuilder(ContentObjectBuilder<?, P, ?> parent, String defaultFormat) {
    this.format = defaultFormat;
    this.parent = Objects.requireNonNull(parent, "Missing parent");
  }

  public ContentBuilder<P> ofType(String type) {
    format = type;
    return this;
  }

  /**
   * Sets the content.
   * @param content the content to add
   * @return this builder
   */
  @SuppressWarnings("unchecked")
  public ContentBuilder<P> as(String content) {
    this.text = content;
    return this;
  }

  /**
   * Sets the content.
   * @param content the content to add
   * @return this builder
   */
  public ContentBuilder<P> as(InputStream content) {
    try {
      return as(IOUtils.toString(content, StandardCharsets.UTF_8));
    } catch (IOException e) {
      throw new IllegalArgumentException("Failed to read content", e);
    }
  }

  /**
   * Sets the content from a named resource.
   * @param name the name of the content resource to add
   * @return this builder
   */
  public ContentBuilder<P> fromResource(String name) {
    try (InputStream content = ContentBuilder.class.getResourceAsStream(name)) {
      return as(content);
    } catch (IOException e) {
      throw new IllegalArgumentException("Failed to read resource: " + name, e);
    }
  }

  @SuppressWarnings("unchecked")
  public P end() {
    parent.addContent(format, text);
    return (P)parent;
  }

  @Override
  public String toString() {
    return "[format: " + format + ", text:" + text + "]";
  }
}


/*
 * Copyright (c) 2016-2017 by OpenText Corporation. All Rights Reserved.
 */
package com.opentext.ia.sdk.support.io;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.function.Function;
import java.util.zip.ZipFile;

/**
 * Process a named entry in a ZIP file.
 */
public class Unzip {

  /**
   * Process a named entry in a given ZIP file.
   * @param file The ZIP file to process
   * @return A new instance for processing the given ZIP file
   */
  public static Unzip file(File file) {
    return new Unzip(file);
  }

  private final File zip;

  /**
   * Process a named entry in a given ZIP file.
   * @param file The ZIP file to process
   */
  public Unzip(File file) {
    this.zip = file;
  }

  /**
   * Process a named entry in the ZIP file.
   * @param <T> The type of result returned by the processor
   * @param entry The name of the entry to process
   * @param processor The processor that handles the entry's content
   * @return The result of processing the entry
   */
  public <T> T andProcessEntry(String entry, Function<InputStream, T> processor) {
    try (ZipFile zipFile = new ZipFile(zip)) {
      return processEntry(zipFile, entry, processor);
    } catch (IOException e) {
      throw new RuntimeIoException(e);
    }
  }

  private <T> T processEntry(ZipFile zipFile, String entry, Function<InputStream, T> processor) throws IOException {
    try (InputStream stream = zipFile.getInputStream(zipFile.getEntry(entry))) {
      return processor.apply(stream);
    }
  }

}


// +-------------------------------------------------------------------------
// | Copyright (C) 2016 Yunify, Inc.
// +-------------------------------------------------------------------------
// | Licensed under the Apache License, Version 2.0 (the "License");
// | you may not use this work except in compliance with the License.
// | You may obtain a copy of the License in the LICENSE file, or 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 com.qingstor.sdk.utils;

import com.qingstor.sdk.constants.QSConstant;
import com.qingstor.sdk.exception.QSException;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * Created by on 11/4/15.
 */
public class QSStringUtil {

    public static String objectToJson(String key, Object o) throws QSException {
        StringBuffer buffer = new StringBuffer("{ \"" + key + "\":");
        buffer.append(objectJSONKeyValue(key, o));
        buffer.append("}");
        return buffer.toString();
    }

    private static String objectJSONKeyValue(String key, Object o) throws QSException {
        StringBuffer buffer = new StringBuffer(" \"" + key + "\":");
        buffer.append(objectJSONValue(o));
        return buffer.toString();
    }

    public static Object objectJSONValue(Object o) throws QSException {

        if (o instanceof List) {
            List lst = (List) o;
            JSONArray jsonArray = new JSONArray();
            for (int i = 0; i < lst.size(); i++) {
                QSJSONUtil.putJsonData(jsonArray, i, objectJSONValue(lst.get(i)));
            }
            return jsonArray;
        } else if (o instanceof Integer
                || o instanceof Double
                || o instanceof Boolean
                || o instanceof Long
                || o instanceof Float
                || o instanceof String) {
            return o;
        } else if (o instanceof Map) {
            return getMapToJson((Map) o);
        } else {
            Map params = new HashMap();
            QSParamInvokeUtil.invokeObject2Map(o.getClass(), o, params);
            return getMapToJson(params);
        }
    }

    public static JSONObject getMapToJson(Map o) throws QSException {
        JSONObject json = new JSONObject();
        try {
            Iterator iterator = o.entrySet().iterator();
            while (iterator.hasNext()) {
                Map.Entry entry = (Map.Entry) iterator.next();
                String key = (String) entry.getKey();
                Object bodyObj = o.get(key);
                json.put(key, objectJSONValue(bodyObj));
            }
        } catch (JSONException e) {
            e.printStackTrace();
            throw new QSException("ObjectToJson", e);
        }

        return json;
    }

    public static String getObjectToJson(Object o) {
        JSONObject json = null;
        if (o instanceof Map) {
            json = new JSONObject();
            Iterator iterator = ((Map) o).entrySet().iterator();
            while (iterator.hasNext()) {
                Map.Entry entry = (Map.Entry) iterator.next();
                String key = (String) entry.getKey();
                Object bodyObj = ((Map) o).get(key);
                json.put(key, bodyObj);
            }
        } else {
            json = new JSONObject(o);
        }

        return json.toString();
    }

    public static String percentEncode(String value, String encoding)
            throws UnsupportedEncodingException {
        return value != null
                ? URLEncoder.encode(value, encoding)
                .replace("+", "%20")
                .replace("*", "%2A")
                .replace("%7E", "~")
                : null;
    }

    public static boolean isEmpty(String str) {
        if (str == null || "".equals(str.trim()) || "null".equalsIgnoreCase(str)) {
            return true;
        }
        return false;
    }

    public static String getUserAgent() {
        String osName = System.getProperty("os.name"); //操作系统名称
        String langVersion = System.getProperty("java.version"); //java.version系统版本
        String userAgent =
                QSConstant.SDK_NAME
                        + "/"
                        + QSConstant.SDK_VERSION
                        + " ( java v"
                        + langVersion
                        + ";"
                        + osName
                        + ")";
        return userAgent;
    }

    public static String getParameterRequired(String paraName, String value) {
        return String.format("%s is required in %s ", paraName, value);
    }

    public static String getParameterValueNotAllowedError(
            String paraName, String value, String[] values) {

        StringBuffer buf = new StringBuffer();
        for (Object o : values) {
            buf.append(o.toString()).append(",");
        }
        if (buf.length() > 0) {
            buf.setLength(buf.length() - 1);
        }

        return String.format(
                "%s value %s is not allowed, should be one of %s ",
                paraName, value, buf.toString());
    }

    /**
     * Chinese characters transform
     *
     * @param str unEncoded chars
     * @return encoded chars
     * @throws QSException UnsupportedEncodingException
     */
    public static String asciiCharactersEncoding(String str) throws QSException {
        if (QSStringUtil.isEmpty(str)) {
            return "";
        }
        try {
            String encoded = URLEncoder.encode(str, QSConstant.ENCODING_UTF8);
            encoded = encoded.replace("%2F", "/");
            encoded = encoded.replace("%3D", "=");
            encoded = encoded.replace("+", "%20");
            encoded = encoded.replace("%3A", ":");
            return encoded;
        } catch (UnsupportedEncodingException e) {
            throw new QSException("UnsupportedEncodingException:", e);
        }
    }

}