AbstractJsonOutputHandler.java

/**
 * Portions of this software was developed by employees of the National Institute
 * of Standards and Technology (NIST), an agency of the Federal Government and is
 * being made available as a public service. Pursuant to title 17 United States
 * Code Section 105, works of NIST employees are not subject to copyright
 * protection in the United States. This software may be subject to foreign
 * copyright. Permission in the United States and in foreign countries, to the
 * extent that NIST may hold copyright, to use, copy, modify, create derivative
 * works, and distribute this software and its documentation without fee is hereby
 * granted on a non-exclusive basis, provided that this notice and disclaimer
 * of warranty appears in all copies.
 *
 * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
 * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
 * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
 * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
 * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
 * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
 * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
 * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
 * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
 * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
 * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
 */

package gov.nist.secauto.swid.builder.output;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;

import gov.nist.secauto.swid.builder.AbstractLanguageSpecificBuilder;
import gov.nist.secauto.swid.builder.EntityBuilder;
import gov.nist.secauto.swid.builder.LinkBuilder;
import gov.nist.secauto.swid.builder.MetaBuilder;
import gov.nist.secauto.swid.builder.Role;
import gov.nist.secauto.swid.builder.SWIDBuilder;
import gov.nist.secauto.swid.builder.ValidationException;
import gov.nist.secauto.swid.builder.VersionScheme;
import gov.nist.secauto.swid.builder.resource.AbstractResourceCollectionBuilder;
import gov.nist.secauto.swid.builder.resource.EvidenceBuilder;
import gov.nist.secauto.swid.builder.resource.HashAlgorithm;
import gov.nist.secauto.swid.builder.resource.PathRelativizer;
import gov.nist.secauto.swid.builder.resource.PayloadBuilder;
import gov.nist.secauto.swid.builder.resource.ResourceBuilder;
import gov.nist.secauto.swid.builder.resource.ResourceCollectionEntryGenerator;
import gov.nist.secauto.swid.builder.resource.file.AbstractFileSystemItemBuilder;
import gov.nist.secauto.swid.builder.resource.file.DirectoryBuilder;
import gov.nist.secauto.swid.builder.resource.file.FileBuilder;
import gov.nist.secauto.swid.builder.resource.firmware.FirmwareBuilder;

import java.io.IOException;
import java.io.OutputStream;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;

public abstract class AbstractJsonOutputHandler extends JsonSupport implements OutputHandler {
  /**
   * The tag identifier (text).
   */
  public static final long TAG_ID_FIELD = 0L;

  /**
   * A name (text).
   */
  public static final long SWID_NAME_FIELD = 1L;
  public static final long ENTITY_FIELD = 2L;
  public static final long EVIDENCE_FIELD = 3L;
  public static final long LINK_FIELD = 4L;
  public static final long SOFTWARE_META_FIELD = 5L;
  public static final long PAYLOAD_FIELD = 6L;
  public static final long CORPUS_FIELD = 8L;
  public static final long PATCH_FIELD = 9L;
  public static final long MEDIA_FIELD = 10L;
  public static final long SUPPLEMENTAL_FIELD = 11L;
  public static final long TAG_VERSION_FIELD = 12L;
  public static final long SOFTWARE_VERSION_FIELD = 13L;
  public static final long VERSION_SCHEME_FIELD = 14L;
  public static final long LANG_FIELD = 15L;
  public static final long DIRECTORY_FIELD = 16L;
  public static final long FILE_FIELD = 17L;
  public static final long PROCESS_FIELD = 18L;
  public static final long RESOURCE_FIELD = 19L;

  /**
   * The size of a file (number: long).
   */
  public static final long SIZE_FIELD = 20L;
  public static final long FILE_VERSION_FIELD = 21L;

  /**
   * (bool).
   */
  public static final long KEY_FIELD = 22L;
  public static final long LOCATION_FIELD = 23L;
  public static final long FS_NAME_FIELD = 24L;
  public static final long ROOT_FIELD = 25L;
  public static final long PATH_ELEMENTS_FIELD = 26L;
  public static final long PROCESS_NAME_FIELD = 27L;
  public static final long PID_FIELD = 28L;
  public static final long TYPE_FIELD = 29L;
  public static final long ENTITY_NAME_FIELD = 31L;
  public static final long REG_ID_FIELD = 32L;

  /**
   * The roles (text: space separated).
   */
  public static final long ROLE_FIELD = 33L;
  public static final long THUMBPRINT_FIELD = 34L;

  public static final long DATE_FIELD = 35L;
  public static final long DEVICE_ID_FIELD = 36L;
  public static final long ARTIFACT_FIELD = 37L;
  public static final long HREF_FIELD = 38L;
  public static final long OWNERSHIP_FIELD = 39L;
  public static final long REL_FIELD = 40L;
  public static final long MEDIA_TYPE_FIELD = 41L;
  public static final long USE_FIELD = 42L;

  public static final long ACTIVATION_STATUS_FIELD = 43L;
  public static final long CHANNEL_TYPE_FIELD = 44L;
  // colloquial-version = (45: text)
  public static final long COLLOQUIAL_VERSION_FIELD = 45L;
  // description = (46: text)
  public static final long DESCRIPTION_FIELD = 46L;
  // edition = (47: text)
  public static final long EDITION_FIELD = 47L;
  // entitlement-data-required = (48: bool)
  public static final long ENTITLEMENT_DATA_REQUIRED_FIELD = 48L;
  // entitlement-key = (49: text)
  public static final long ENTITLEMENT_KEY_FIELD = 49L;
  // generator = (50: text)
  public static final long GENERATOR_FIELD = 50L;
  // persistent-id = (51: text)
  public static final long PERSISTENT_ID_FIELD = 51L;
  // product = (52: text)
  public static final long PRODUCT_FIELD = 52L;
  // product-family = (53: text)
  public static final long PRODUCT_FAMILY_FIELD = 53L;
  // revision = (54: text)
  public static final long REVISION_FIELD = 54L;
  // summary = (55: text)
  public static final long SUMMARY_FIELD = 55L;
  // unspsc-code = (56: text)
  public static final long UNSPSC_CODE_FIELD = 56L;
  // unspsc-version = (57: text)
  public static final long UNSPSC_VERSION_FIELD = 57L;

  public static final long HASH_FIELD = 7L;

  /**
   * Firmware.
   */
  public static final long FIRMWARE_FIELD = 59L;

  private final JsonFactory jsonFactory;

  public AbstractJsonOutputHandler(JsonFactory jsonFactory) {
    this.jsonFactory = jsonFactory;
  }

  protected abstract void writeRole(JsonGenerator generator, Role role) throws IOException;

  protected abstract void writeVersionScheme(JsonGenerator generator, VersionScheme versionScheme) throws IOException;

  /**
   * @return the jsonFactory
   */
  public JsonFactory getJsonFactory() {
    return jsonFactory;
  }

  protected JsonGenerator newGenerator(OutputStream os) throws IOException {

    JsonFactory factory = getJsonFactory();

    JsonGenerator generator = factory.createGenerator(os);
    return generator;
  }

  @Override
  public void write(SWIDBuilder builder, OutputStream os) throws IOException, ValidationException {
    builder.validate();

    JsonGenerator generator = newGenerator(os);

    build(generator, builder);

    generator.close();
  }

  protected void build(JsonGenerator generator, SWIDBuilder builder) throws IOException {
    generator.writeStartObject();

    buildGlobalAttributes(generator, builder);

    // required attributes
    writeTextField(generator, TAG_ID_FIELD, builder.getTagId());

    writeIntegerField(generator, TAG_VERSION_FIELD, builder.getTagVersion());

    writeTextField(generator, SWID_NAME_FIELD, builder.getName());
    if (builder.getVersion() != null) {
      writeTextField(generator, SOFTWARE_VERSION_FIELD, builder.getVersion());
    }
    VersionScheme versionScheme = builder.getVersionScheme();
    if (versionScheme != null) {
      writeField(generator, VERSION_SCHEME_FIELD);
      writeVersionScheme(generator, versionScheme);
    }

    // optional attribute
    switch (builder.getTagType()) {
    case PRIMARY:
      break;
    case CORPUS:
      writeBooleanField(generator, CORPUS_FIELD, true);
      break;
    case PATCH:
      writeBooleanField(generator, PATCH_FIELD, true);
      break;
    case SUPPLEMENTAL:
      writeBooleanField(generator, SUPPLEMENTAL_FIELD, true);
      break;
    default:
      throw new IllegalStateException("tagType: " + builder.getTagType().toString());
    }

    // child elements
    // Required
    writeField(generator, ENTITY_FIELD);
    List<EntityBuilder> entities = builder.getEntities();
    if (entities.size() > 1) {
      generator.writeStartArray();
    }

    for (EntityBuilder entity : builder.getEntities()) {
      build(generator, entity);
    }

    if (entities.size() > 1) {
      generator.writeEndArray();
    }

    // optional
    EvidenceBuilder evidence = builder.getEvidence();
    if (evidence != null) {
      writeField(generator, EVIDENCE_FIELD);
      build(generator, evidence);
    }

    List<LinkBuilder> links = builder.getLinks();
    if (!links.isEmpty()) {
      writeField(generator, LINK_FIELD);
      if (links.size() > 1) {
        generator.writeStartArray();
      }
      for (LinkBuilder link : links) {
        build(generator, link);
      }
      if (links.size() > 1) {
        generator.writeEndArray();
      }
    }

    List<MetaBuilder> metas = builder.getMetas();
    if (!links.isEmpty()) {
      writeField(generator, SOFTWARE_META_FIELD);
      if (metas.size() > 1) {
        generator.writeStartArray();
      }
      for (MetaBuilder meta : metas) {
        buildMeta(generator, meta);
      }
      if (metas.size() > 1) {
        generator.writeEndArray();
      }
    }

    PayloadBuilder payload = builder.getPayload();
    if (payload != null) {
      writeField(generator, PAYLOAD_FIELD);
      buildPayload(generator, payload);
    }

    // TODO: media
    //
    // generator.writeStringField("test", "test");
    generator.writeEndObject();
  }

  protected void build(JsonGenerator generator, EntityBuilder builder) throws IOException {

    // start of the entity
    generator.writeStartObject();

    buildGlobalAttributes(generator, builder);

    writeTextField(generator, ENTITY_NAME_FIELD, builder.getName());
    if (builder.getRegid() != null) {
      writeTextField(generator, REG_ID_FIELD, builder.getRegid());
    }

    List<Role> roles = builder.getRoles();
    writeField(generator, ROLE_FIELD);
    if (roles.size() > 1) {
      generator.writeStartArray();
    }
    for (Role role : roles) {
      writeRole(generator, role);
    }
    if (roles.size() > 1) {
      generator.writeEndArray();
    }

    if (builder.getThumbprint() != null) {
      writeTextField(generator, THUMBPRINT_FIELD, builder.getThumbprint());
    }

    // for empty meta
    // writeField(generator, META_ELEMENTS_FIELD);
    // generator.writeStartArray();
    // generator.writeEndArray();

    // end of the entity
    generator.writeEndObject();
  }

  private void build(JsonGenerator generator, EvidenceBuilder builder) throws IOException {

    // start of the evidence
    generator.writeStartObject();

    buildGlobalAttributes(generator, builder);

    buildResourceCollection(generator, builder);

    ZonedDateTime date = builder.getDate();
    if (date != null) {
      writeDateTimeField(generator, DATE_FIELD, date);
    }

    if (builder.getDeviceId() != null) {
      writeTextField(generator, DEVICE_ID_FIELD, builder.getDeviceId());
    }

    // end of the evidence
    generator.writeEndObject();
  }

  private void build(JsonGenerator generator, LinkBuilder builder) throws IOException {

    // start of the link
    generator.writeStartObject();

    buildGlobalAttributes(generator, builder);

    // required attributes
    writeTextField(generator, HREF_FIELD, builder.getHref().toString());
    writeTextField(generator, REL_FIELD, builder.getRel());

    // optional attributes
    if (builder.getArtifact() != null) {
      writeTextField(generator, ARTIFACT_FIELD, builder.getArtifact());
    }
    if (builder.getMedia() != null) {
      writeTextField(generator, MEDIA_FIELD, builder.getMedia());
    }
    if (builder.getOwnership() != null) {
      writeTextField(generator, OWNERSHIP_FIELD, builder.getOwnership().toString());
    }
    if (builder.getMediaType() != null) {
      writeTextField(generator, MEDIA_TYPE_FIELD, builder.getMediaType());
    }
    if (builder.getUse() != null) {
      writeTextField(generator, USE_FIELD, builder.getUse().toString());
    }

    // end of the link
    generator.writeEndObject();
  }

  private void buildMeta(JsonGenerator generator, MetaBuilder builder) throws IOException {

    // start of the meta
    generator.writeStartObject();

    buildGlobalAttributes(generator, builder);

    buildAttribute(generator, ACTIVATION_STATUS_FIELD, builder.getActivationStatus());
    buildAttribute(generator, CHANNEL_TYPE_FIELD, builder.getChannelType());
    buildAttribute(generator, COLLOQUIAL_VERSION_FIELD, builder.getColloquialVersion());
    buildAttribute(generator, DESCRIPTION_FIELD, builder.getDescription());
    buildAttribute(generator, EDITION_FIELD, builder.getEdition());
    buildAttribute(generator, ENTITLEMENT_DATA_REQUIRED_FIELD, builder.getEntitlementDataRequired());
    buildAttribute(generator, ENTITLEMENT_KEY_FIELD, builder.getEntitlementKey());
    buildAttribute(generator, GENERATOR_FIELD, builder.getGenerator());
    buildAttribute(generator, PERSISTENT_ID_FIELD, builder.getPersistentId());
    buildAttribute(generator, PRODUCT_FIELD, builder.getProductBaseName());
    buildAttribute(generator, PRODUCT_FAMILY_FIELD, builder.getProductFamily());
    buildAttribute(generator, REVISION_FIELD, builder.getRevision());
    buildAttribute(generator, SUMMARY_FIELD, builder.getSummary());
    buildAttribute(generator, UNSPSC_CODE_FIELD, builder.getUnspscCode());
    buildAttribute(generator, UNSPSC_VERSION_FIELD, builder.getUnspscVersion());

    // end of the meta
    generator.writeEndObject();
  }

  private void buildAttribute(JsonGenerator generator, long fieldId, String value) throws IOException {
    if (value != null) {
      writeTextField(generator, fieldId, value);
    }
  }

  private void buildPayload(JsonGenerator generator, PayloadBuilder builder) throws IOException {

    // start of the payload
    generator.writeStartObject();

    buildGlobalAttributes(generator, builder);

    buildResourceCollection(generator, builder);

    // end of the payload
    generator.writeEndObject();
  }

  private <E extends AbstractResourceCollectionBuilder<E>> void buildResourceCollection(JsonGenerator generator,
      AbstractResourceCollectionBuilder<E> builder) throws IOException {
    buildGlobalAttributes(generator, builder);

    JsonResourceCollectionEntryGenerator creator = new JsonResourceCollectionEntryGenerator();
    {
      List<DirectoryBuilder> directories = builder.getResources(DirectoryBuilder.class);
      if (!directories.isEmpty()) {
        writeDirectories(generator, directories, creator);
      }
    }
    {
      List<FileBuilder> files = builder.getResources(FileBuilder.class);
      if (!files.isEmpty()) {
        writeFiles(generator, files, creator);
      }
    }
    {
      List<FirmwareBuilder> firmwares = builder.getResources(FirmwareBuilder.class);
      if (!firmwares.isEmpty()) {
        writeFirmware(generator, firmwares, creator);
      }
    }
  }

  private void writeDirectories(JsonGenerator generator, List<DirectoryBuilder> directories,
      JsonResourceCollectionEntryGenerator creator) throws IOException {
    writeField(generator, DIRECTORY_FIELD);
    writeResources(generator, directories, creator);
  }

  private void writeFiles(JsonGenerator generator, List<FileBuilder> files,
      JsonResourceCollectionEntryGenerator creator) throws IOException {
    writeField(generator, FILE_FIELD);
    writeResources(generator, files, creator);
  }

  private void writeFirmware(JsonGenerator generator, List<FirmwareBuilder> firmwares,
      JsonResourceCollectionEntryGenerator creator) throws IOException {
    writeField(generator, FIRMWARE_FIELD);
    writeResources(generator, firmwares, creator);
  }

  private void writeResources(JsonGenerator generator, List<? extends ResourceBuilder> resources,
      JsonResourceCollectionEntryGenerator creator) throws IOException {
    // use an array for more than one
    if (resources.size() > 1) {
      generator.writeStartArray();
    }
    for (ResourceBuilder builder : resources) {
      builder.accept(generator, creator);
    }
    if (resources.size() > 1) {
      generator.writeEndArray();
    }
  }

  private void writeHash(JsonGenerator generator, HashAlgorithm algorithm, byte[] bytes) throws IOException {
    writeField(generator, HASH_FIELD);
    generator.writeStartArray();
    generator.writeNumber(algorithm.getIndex());
    generator.writeBinary(bytes);
    generator.writeEndArray();
  }

  private <E extends AbstractLanguageSpecificBuilder<E>> void buildGlobalAttributes(JsonGenerator generator,
      AbstractLanguageSpecificBuilder<E> builder) throws IOException {
    String language = builder.getLanguage();
    if (language != null) {
      writeTextField(generator, LANG_FIELD, language);
    }
  }

  private class JsonResourceCollectionEntryGenerator implements ResourceCollectionEntryGenerator<JsonGenerator> {

    public JsonResourceCollectionEntryGenerator() {
    }

    @Override
    public void generate(JsonGenerator parent, DirectoryBuilder builder) {
      try {
        parent.writeStartObject();

        buildGlobalAttributes(parent, builder);

        buildFileSystemItem(parent, builder);

        {
          List<DirectoryBuilder> directories = builder.getResources(DirectoryBuilder.class);
          if (!directories.isEmpty()) {
            writeDirectories(parent, directories, this);
          }
        }
        {
          List<FileBuilder> files = builder.getResources(FileBuilder.class);
          if (!files.isEmpty()) {
            writeFiles(parent, files, this);
          }
        }

        parent.writeEndObject();
      } catch (IOException ex) {
        throw new RuntimeException(ex);
      }
    }

    @Override
    public void generate(JsonGenerator parent, FileBuilder builder) {

      try {
        parent.writeStartObject();

        buildGlobalAttributes(parent, builder);

        buildFileSystemItem(parent, builder);

        if (builder.getSize() != null) {
          writeLongField(parent, SIZE_FIELD, builder.getSize());
        }
        if (builder.getVersion() != null) {
          writeTextField(parent, SOFTWARE_VERSION_FIELD, builder.getVersion());
        }

        Map<HashAlgorithm, byte[]> hashMap = builder.getHashAlgorithmToValueMap();
        if (!hashMap.isEmpty()) {
          if (hashMap.size() > 1) {
            throw new UnsupportedOperationException("Only a single hash value is allowed");
          }

          Map.Entry<HashAlgorithm, byte[]> entry = hashMap.entrySet().iterator().next();
          writeHash(parent, entry.getKey(), entry.getValue());
        }

        // end of the payload
        parent.writeEndObject();
      } catch (IOException ex) {
        throw new RuntimeException(ex);
      }
    }

    @Override
    public void generate(JsonGenerator generator, FirmwareBuilder builder) {
      new CBORFirmwareOutputHandler().generate(generator, builder);
    }

    private <E extends AbstractFileSystemItemBuilder<E>> void buildFileSystemItem(JsonGenerator parent,
        AbstractFileSystemItemBuilder<E> builder) throws IOException {

      // TODO: support meta

      if (builder.getKey() != null) {
        writeBooleanField(parent, KEY_FIELD, builder.getKey());
      }

      if (builder.getRoot() != null) {
        writeTextField(parent, ROOT_FIELD, builder.getRoot());
      }

      List<String> location = builder.getLocation();
      if (location != null && !location.isEmpty()) {
        writeTextField(parent, LOCATION_FIELD, PathRelativizer.toURI(location).toString());
      }
      writeTextField(parent, FS_NAME_FIELD, builder.getName());
    }
  }
}