/** * 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; }