/**
     * Create a custom field ordering from a list of strings representing field labels. There may
     * also be one or more optionally-quoted strings, each indicating a value to be inserted into a
     * constant-valued column.
     *
     * @param labels a list of field labels and optionally constant string values
     * @throws CustomColumnOrderingException if there is an unrecognised field name or no id fields
     */
    public CustomColumnOrdering(List<String> labels) throws CustomColumnOrderingException {
      this.ordering = ReportColumn.fromStrings(labels);
      // Note we regenerate the labels from the ReportColumns so constant-valued
      // columns are consistently unquoted.
      this.orderedLabels = ReportColumn.getLabels(ordering);

      // Create the Field ordering
      this.fieldOrdering =
          new ArrayList<Field>() {
            {
              for (ReportColumn c : ordering) {
                if (c.isField()) add(c.field);
              }
            }
          };

      // Sanity checks
      // Are there any fields included?
      if (this.fieldOrdering.isEmpty())
        throw new CustomColumnOrderingException("", "No valid fields specified.");
      // Are there id fields included?
      if (!Field.idFields.clone().removeAll(this.fieldOrdering)) {
        String idFieldStr =
            String.format("(%s)", StringUtils.join(Field.getLabels(Field.idFields), ", "));
        throw new CustomColumnOrderingException(
            "",
            String.format(
                "No identifying fields specified. You must include one of %s.", idFieldStr));
      }
      // Set the field set if all is okay
      this.fields = EnumSet.copyOf(this.fieldOrdering);
    }
 /**
  * Remove the named columns from the ordering. Returns the ordering for convenience.
  *
  * @param columns a collection of labels
  * @return
  */
 protected CustomColumnOrdering remove(Collection<String> columns) {
   this.ordering.removeAll(ReportColumn.fromStrings(columns));
   this.orderedLabels.removeAll(columns);
   Collection<Field> flds = Field.getFields(columns);
   this.fieldOrdering.removeAll(flds);
   this.fields.removeAll(flds);
   return this;
 }
 /**
  * Create a column from the name, treating it as a Field if the name matches a field name, or a
  * custom string otherwise. If it is a custom string, it is automatically unquoted.
  *
  * @param name the label of a Field, or a String with or without quotes
  */
 public ReportColumn(String name) {
   name = name.trim();
   try {
     this.field = Field.valueOf(name.toUpperCase());
     this.label = null;
   } catch (IllegalArgumentException e) {
     this.field = null;
     this.label = unquote(name);
   }
 }
 /**
  * Calculate which fields have no values across the whole range of titles. Iterates through all
  * the titles for each field value, until an entry is found or the end of the title list is
  * reached.
  *
  * @return a set of fields which are empty
  */
 private EnumSet<Field> findEmptyFields() {
   long s = System.currentTimeMillis();
   EnumSet<Field> empty = EnumSet.allOf(Field.class);
   for (Field f : Field.getFieldSet()) {
     // Check if any title has a value for this field
     for (KbartTitle kbt : titles) {
       if (kbt.hasFieldValue(f)) {
         empty.remove(f);
         break;
       }
     }
   }
   // Log the time, as this is an expensive way to monitor empty fields.
   // It should be done somehow during title conversion.
   log.debug("findEmptyFields() took " + (System.currentTimeMillis() - s) + "s");
   return empty;
 }
  /**
   * An enum for specifying a variety of <code>FieldOrdering</code>s. It is possible to specify an
   * incomplete list of fields, and other fields will be omitted.
   *
   * @author Neil Mayo
   */
  public static enum PredefinedColumnOrdering implements ColumnOrdering {

    // Default KBART ordering
    KBART(
        "KBART Default",
        "Full KBART format in default ordering; one line per coverage range",
        Field.values()),

    // An ordering that puts publisher and publication first; mainly for use by Vicky
    PUBLISHER_PUBLICATION(
        "Publisher oriented",
        "Standard KBART fields with publisher name at the start",
        new Field[] {
          PUBLISHER_NAME,
          PUBLICATION_TITLE,
          PRINT_IDENTIFIER,
          ONLINE_IDENTIFIER,
          DATE_FIRST_ISSUE_ONLINE,
          NUM_FIRST_VOL_ONLINE,
          NUM_FIRST_ISSUE_ONLINE,
          DATE_LAST_ISSUE_ONLINE,
          NUM_LAST_VOL_ONLINE,
          NUM_LAST_ISSUE_ONLINE,
          TITLE_URL,
          FIRST_AUTHOR,
          TITLE_ID,
          EMBARGO_INFO,
          COVERAGE_DEPTH,
          COVERAGE_NOTES
        }),

    // An ordering that puts publisher and publication first, and only shows
    // title identifying information and coverage ranges.
    TITLE_COVERAGE_RANGES(
        "Title Coverage Ranges",
        "List coverage ranges for each title; one per line, publisher first",
        new Field[] {
          PUBLISHER_NAME,
          PUBLICATION_TITLE,
          PRINT_IDENTIFIER,
          ONLINE_IDENTIFIER,
          DATE_FIRST_ISSUE_ONLINE,
          NUM_FIRST_VOL_ONLINE,
          NUM_FIRST_ISSUE_ONLINE,
          DATE_LAST_ISSUE_ONLINE,
          NUM_LAST_VOL_ONLINE,
          NUM_LAST_ISSUE_ONLINE
        }),

    // Minimal details - Title identifiers
    TITLES_BASIC(
        "Basic Title Details",
        "Publisher, Publication, ISSN, eISSN",
        new Field[] {PUBLISHER_NAME, PUBLICATION_TITLE, PRINT_IDENTIFIER, ONLINE_IDENTIFIER}),

    // Title and ISSN only (will have duplicates)
    TITLE_ISSN(
        "Title and ISSN",
        "List ISSNs and Title only",
        new Field[] {PRINT_IDENTIFIER, PUBLICATION_TITLE}),

    // ISSN only (will have duplicates)
    ISSN_ONLY("ISSN only", "Produce a list of ISSNs", PRINT_IDENTIFIER),

    // TITLE_ID only - should have an id for every record
    //    TITLE_ID_ONLY("Title ID only", "Produce a list of unique identifiers",
    //        TITLE_ID
    //    ),

    // SFX fields only
    SFX(
        "SFX Fields",
        "Produce a list of fields for SFX DataLoader",
        //        TITLE_ID,
        PRINT_IDENTIFIER, // PJG
        ONLINE_IDENTIFIER,
        "ACTIVE",
        COVERAGE_NOTES),
    ;

    /** Store the field ordering internally as a CustomColumnOrdering. */
    private CustomColumnOrdering customColumnOrdering;

    // These are used in an interface to describe the ordering. */
    /** A name to identify the ordering. */
    public final String displayName;
    /** A description of the ordering. */
    public final String description;

    public EnumSet<Field> getFields() {
      return customColumnOrdering.getFields();
    }

    public List<Field> getOrderedFields() {
      return customColumnOrdering.getOrderedFields();
    }

    public List<String> getOrderedLabels() {
      return customColumnOrdering.getOrderedLabels();
    }

    public List<ReportColumn> getOrderedColumns() {
      return customColumnOrdering.getOrderedColumns();
    }

    public String toString() {
      return ""
          + StringUtil.separatedString(getOrderedLabels(), "PredefinedColumnOrdering(", " | ", ")");
    }

    /**
     * Return a list of the column labels which do not refer to a Field.
     *
     * @return
     */
    public List<String> getNonFieldColumnLabels() {
      return new ArrayList<String>() {
        {
          for (ReportColumn rc : getOrderedColumns()) {
            if (!rc.isField()) add(rc.getLabel());
          }
        }
      };
    }

    /**
     * Make a predefined ordering from a list of objects. This is a convenience method to make it
     * easier to specify predefined lists in this enum with mixed string/field columns.
     *
     * @param displayName the display name of the ordering
     * @param description a description for the ordering
     * @param columns a list of objects whose string representation will define the ordering
     * @throws
     *     org.lockss.exporter.kbart.KbartExportFilter.CustomColumnOrdering.CustomColumnOrderingException
     *     if there is a problem creating the internal ColumnOrdering
     */
    private PredefinedColumnOrdering(
        final String displayName, final String description, final Object... columns) {
      this(displayName, description, CustomColumnOrdering.createUnchecked(columns));
    }

    /**
     * Constructor for ordering based on aan array of Fields. This is the safest constructor as it
     * uses enums and therefore needs no checking.
     *
     * @param displayName
     * @param description
     * @param fieldOrder
     */
    PredefinedColumnOrdering(
        final String displayName, final String description, final Field[] fieldOrder) {
      this(displayName, description, CustomColumnOrdering.create(fieldOrder));
    }

    /**
     * Create an ordering with a name, description, and a pre-built CustomColumnOrdering.
     *
     * @param displayName the display name of the ordering
     * @param description a description for the ordering
     * @param customColumnOrdering the custom ordering
     */
    private PredefinedColumnOrdering(
        final String displayName,
        final String description,
        final CustomColumnOrdering customColumnOrdering) {
      this.displayName = displayName;
      this.description = description;
      this.customColumnOrdering = customColumnOrdering;
    }
  }
  /**
   * A custom column ordering allows an arbitrary order of ReportColumns to be specified at
   * construction. This can include a limited number of constant-valued columns. It will also allow
   * duplicated Field columns.
   *
   * @author Neil Mayo
   */
  public static class CustomColumnOrdering implements ColumnOrdering {
    /** A set of fields included in this ordering. */
    protected final EnumSet<Field> fields;
    /** An ordered list of ReportColumns in this ordering. */
    protected final List<ReportColumn> ordering;
    /** An ordered list of Fields in this ordering. */
    protected final List<Field> fieldOrdering;
    /** An ordered list of labels for the fields. */
    protected final List<String> orderedLabels;
    /**
     * A separator used to separate field names in the specification of a custom ordering. This may
     * be any combination of whitespace.
     */
    public static final String CUSTOM_ORDERING_FIELD_SEPARATOR = "\n";
    /**
     * A regular expression representing characters used to separate field names in the
     * specification of a custom ordering. Splits on line break and/or carriage return.
     */
    private static final String CUSTOM_ORDERING_FIELD_SEPARATOR_REGEX = "[\n\r]+";
    /**
     * The maximum allowable number of columns from a user-defined list; when splitting the list,
     * any tokens beyond this number will be discarded. Practically, this means it is possible to
     * have up to MAX_COLUMNS-1 user-defined constant value columns (as there must be one id
     * column).
     */
    private static final int MAX_COLUMNS = Field.values().length * 2;

    public static ColumnOrdering getDefaultOrdering() {
      return PredefinedColumnOrdering.KBART;
    }
    // Getters
    public List<ReportColumn> getOrderedColumns() {
      return ordering;
    }

    public EnumSet<Field> getFields() {
      return this.fields;
    }

    public List<Field> getOrderedFields() {
      return this.fieldOrdering;
    }

    public List<String> getOrderedLabels() {
      return orderedLabels;
    }

    /**
     * Convenience method to create a custom field ordering from an array of Fields.
     *
     * @param ordering an ordered array of Fields
     */
    public static CustomColumnOrdering create(final Field[] ordering) {
      return create(Arrays.asList(ordering));
    }
    /**
     * Convenience method to create a custom field ordering from a list of Fields.
     *
     * @param fieldOrdering an ordered list of Fields
     */
    public static CustomColumnOrdering create(final List<Field> fieldOrdering) {
      final List<ReportColumn> ordering = ReportColumn.fromFields(fieldOrdering);
      return new CustomColumnOrdering(ordering, fieldOrdering);
    }

    /**
     * Create a custom column ordering from the columns in another column ordering.
     *
     * @param other
     * @return
     */
    public static CustomColumnOrdering copy(ColumnOrdering other) {
      return createUnchecked(other.getOrderedLabels());
    }

    /**
     * Create an ordering from predefined lists of Fields and columns. This is enough information
     * from which to interpolate the other final members. Because there is no String interpretation,
     * there is no need to generate or catch exceptions. This version includes the ordered list of
     * labels, to save processing for those cases where the caller has already generated the list.
     *
     * @param fieldOrdering
     * @param ordering
     * @param orderedLabels
     */
    private CustomColumnOrdering(
        final List<ReportColumn> ordering,
        final List<Field> fieldOrdering,
        final List<String> orderedLabels) {
      this.ordering = ordering;
      this.fieldOrdering = fieldOrdering;
      this.orderedLabels = orderedLabels;
      this.fields = EnumSet.copyOf(fieldOrdering);
    }

    /**
     * Create an ordering from predefined lists of Fields and columns. This is enough information
     * from which to interpolate the other final members. Because there is no String interpretation,
     * there is no need to generate or catch exceptions. It is possible to get the list of fields
     * from the list of ReportColumns, but there is already a constructor with a single List
     * argument.
     *
     * @param fieldOrdering
     * @param ordering
     */
    protected CustomColumnOrdering(
        final List<ReportColumn> ordering, final List<Field> fieldOrdering) {
      this(
          ordering,
          fieldOrdering,
          new ArrayList<String>() {
            {
              for (ReportColumn rc : ordering) add(rc.getLabel());
            }
          });
    }

    /**
     * Create a custom field ordering from a list of strings representing field labels. There may
     * also be one or more optionally-quoted strings, each indicating a value to be inserted into a
     * constant-valued column.
     *
     * @param labels a list of field labels and optionally constant string values
     * @throws CustomColumnOrderingException if there is an unrecognised field name or no id fields
     */
    public CustomColumnOrdering(List<String> labels) throws CustomColumnOrderingException {
      this.ordering = ReportColumn.fromStrings(labels);
      // Note we regenerate the labels from the ReportColumns so constant-valued
      // columns are consistently unquoted.
      this.orderedLabels = ReportColumn.getLabels(ordering);

      // Create the Field ordering
      this.fieldOrdering =
          new ArrayList<Field>() {
            {
              for (ReportColumn c : ordering) {
                if (c.isField()) add(c.field);
              }
            }
          };

      // Sanity checks
      // Are there any fields included?
      if (this.fieldOrdering.isEmpty())
        throw new CustomColumnOrderingException("", "No valid fields specified.");
      // Are there id fields included?
      if (!Field.idFields.clone().removeAll(this.fieldOrdering)) {
        String idFieldStr =
            String.format("(%s)", StringUtils.join(Field.getLabels(Field.idFields), ", "));
        throw new CustomColumnOrderingException(
            "",
            String.format(
                "No identifying fields specified. You must include one of %s.", idFieldStr));
      }
      // Set the field set if all is okay
      this.fields = EnumSet.copyOf(this.fieldOrdering);
    }

    /**
     * Create a custom field ordering from a string containing a list of field labels separated by
     * whitespace (specifically the <code>CUSTOM_ORDERING_FIELD_SEPARATOR_REGEX</code>).
     *
     * <p>The ordering string must contain only valid field names separated by line returns ("\n"
     * and/or "\r"), except for one exception; if it contains a quoted string on its own line, an
     * extra column will be created in that position, containing only that string in every position
     * including the header row. The string can include escaped characters.
     *
     * @param orderStr a string of field labels separated by whitespace
     */
    public CustomColumnOrdering(String orderStr) throws CustomColumnOrderingException {
      this(splitCustomOrderingString(orderStr));
    }

    /**
     * Create a custom ordering without generating exceptions on invalid specification. This method
     * will create a custom ordering as per the list of labels. Labels that do not match fields will
     * be included as constant-valued columns, and there are no restrictions on whether id fields
     * are included (allowing for the creation of orderings leading to useless reports); nor are
     * there limits on the number of columns which can be included. This method should therefore
     * only be used in creating enumerated PredefinedFieldOrderings, or user-defined orderings
     * defined through the LOCKSS user interface.
     *
     * @param labels
     * @return
     */
    protected static CustomColumnOrdering createUnchecked(List<String> labels) {
      final List<ReportColumn> ordering = ReportColumn.fromStrings(labels);
      List<Field> fieldOrdering =
          new ArrayList<Field>() {
            {
              for (ReportColumn c : ordering) {
                if (c.isField()) add(c.field);
              }
            }
          };
      return new CustomColumnOrdering(ordering, fieldOrdering, labels);
    }

    /**
     * Convenience method delegating to createUnchecked(). Takes a list of objects to make it easier
     * to specify predefined lists with a mix of string and field columns.
     *
     * @param columns a list of objects whose string representation will define the ordering
     * @return
     */
    protected static CustomColumnOrdering createUnchecked(final Object... columns) {
      return createUnchecked(
          new ArrayList<String>() {
            {
              for (Object o : columns) add(o.toString());
            }
          });
    }

    /**
     * Split a CustomOrdering string, like those supplied via the user interface, into individual
     * strings. This does not check whether those strings are valid field names; it is just a
     * convenience method. Individual tokens are stripped of whitespace, and empty strings are
     * omitted.
     *
     * @param orderStr
     * @return
     */
    public static List<String> splitCustomOrderingString(String orderStr) {
      String[] arr = orderStr.split(CUSTOM_ORDERING_FIELD_SEPARATOR_REGEX, MAX_COLUMNS);
      List<String> list = new ArrayList<String>();
      // Trim whitespace, and omit empties
      for (String s : arr) {
        s = s.trim();
        if (s.length() != 0) list.add(s);
      }
      return list;
    }

    public String toString() {
      return "" + StringUtil.separatedString(orderedLabels, "CustomColumnOrdering(", " | ", ")");
    }

    /**
     * Remove the named columns from the ordering. Returns the ordering for convenience.
     *
     * @param columns a collection of labels
     * @return
     */
    protected CustomColumnOrdering remove(Collection<String> columns) {
      this.ordering.removeAll(ReportColumn.fromStrings(columns));
      this.orderedLabels.removeAll(columns);
      Collection<Field> flds = Field.getFields(columns);
      this.fieldOrdering.removeAll(flds);
      this.fields.removeAll(flds);
      return this;
    }

    /**
     * Remove the named Fields from the ordering. Returns the ordering for convenience.
     *
     * @param fieldsToRemove a collection of fields
     * @return
     */
    protected CustomColumnOrdering removeFields(Collection<Field> fieldsToRemove) {
      List<ReportColumn> cols = ReportColumn.fromFields(fieldsToRemove);
      this.ordering.removeAll(cols);
      this.orderedLabels.removeAll(ReportColumn.getLabels(cols));
      this.fieldOrdering.removeAll(fieldsToRemove);
      this.fields.removeAll(fieldsToRemove);
      return this;
    }

    /**
     * A custom exception caused when an invalid custom ordering string is passed to the constructor
     * of the CustomColumnOrdering.
     */
    public static class CustomColumnOrderingException extends Exception {
      /** A record of the string label for which a field could not be found. */
      private final String errorLabel;

      public CustomColumnOrderingException(String s) {
        super();
        this.errorLabel = s;
      }

      public CustomColumnOrderingException(String s, String msg) {
        super(msg);
        this.errorLabel = s;
      }

      public String getErrorLabel() {
        return errorLabel;
      }
    }
  }
 /**
  * Whether fields were omitted manually. That is, the specified field ordering is shorter than the
  * available number of fields.
  *
  * @return whether empty fields were omitted.
  */
 public boolean omittedFieldsManually() {
   return columnOrdering.getFields().size() < Field.values().length;
 }
 /** Return the label of the column, or the constant value. */
 public String getLabel() {
   return isField() ? field.getLabel() : label;
 }