From 61d181bb2d9d6342e74d7407c77758b216106ecd Mon Sep 17 00:00:00 2001 From: Saurabh Parkhi Date: Fri, 12 Apr 2024 10:26:33 +0100 Subject: [PATCH] Implementation for the export template work. (#1081) * Initial commit for export templates. Added template classes for basic wiring and changed writer to use maps instead of lists to use column names as we will be unable to rely cannot rely on column order * Column order through template implmented for per file as well as per format export * Created column def classes and used them through into csv writer * Multiple column defs and fixed a bug in existing code where blank values were not written for additional headers with suffix * Header labels used from the template * Constant string data column writing functionality added to template writing * DataModifier column added and wired up in the backend * UI and CLI wired up to convert individual params into wrapper object to redue number of parameters on ExportProfiles method * Added panels to UI and made the UI selectable between columns and templates. Wired up template selection to template use for export * Added panels to UI and made the UI selectable between columns and templates. Wired up template selection to template use for export * Constant string data added to the per format export * UI updated to adjust right hand panel border * Few corrections and added a new class for combobox sorting * default selection code for combo box moved to the consumer of ComboBox * Refactored the logic for data writing into separate classes to get cleaner separation * Minor name changes * Added javadoc and removed some hardcoded, unused lines. * Added unit test for combo box and some ui changes. * Updated a few javadoc comments * Updated a few javadoc comments * missing algorithm added * New screenshot and explanation in the help pages * removed the commented leftover code * Added CLI support * More changes CLI for export template * Documentation update * Fixed NPE when underlying data was null, added few tests * documentation link mismatch fixed for CLI * blank space header was not producing error. * Few unit tests related to colon in the values. * Modified to throw clearer error messages * Modified to throw clearer error messages * clarified help for export template * Typo corrected * Changes following code review --------- Co-authored-by: Sam Palmer --- .../command/action/CommandFactoryImpl.java | 8 + .../command/action/CommandLineParam.java | 19 + .../droid/command/action/ExportCommand.java | 42 +- .../droid/command/i18n/I18N.java | 6 +- .../src/main/resources/options.properties | 3 +- .../command/action/ExportCommandTest.java | 29 +- .../interfaces/config/DroidGlobalConfig.java | 14 +- .../export/interfaces/ExportDetails.java | 159 ++++ .../export/interfaces/ExportManager.java | 10 +- .../export/interfaces/ExportTemplate.java | 47 ++ .../interfaces/ExportTemplateColumnDef.java | 105 +++ .../droid/export/interfaces/ItemWriter.java | 7 + .../droid/export/ExportManagerImpl.java | 28 +- .../template/ConstantStringColumnDef.java | 93 +++ .../template/DataModifierColumnDef.java | 111 +++ .../template/ExportTemplateBuilder.java | 201 +++++ .../export/template/ExportTemplateImpl.java | 52 ++ .../ExportTemplateParseException.java | 43 ++ .../ProfileResourceNodeColumnDef.java | 94 +++ .../export/ExportJobIntegrationTest.java | 13 +- .../droid/export/ExportManagerImplTest.java | 42 +- .../template/ConstantStringColumnDefTest.java | 75 ++ .../template/DataModifierColumnDefTest.java | 70 ++ .../template/ExportTemplateBuilderTest.java | 286 +++++++ .../ProfileResourceNodeColumnDefTest.java | 76 ++ .../Images/Export dialog template.png | Bin 0 -> 59773 bytes .../main/resources/Images/Export dialog.png | Bin 21212 -> 89418 bytes .../Web pages/Command line control.html | 35 +- .../Web pages/Exporting profiles.html | 116 ++- .../droid/profile/CsvItemWriter.java | 327 ++------ .../droid/profile/CsvWriterConstants.java | 170 +++++ .../datawriter/ColumnBasedDataWriter.java | 163 ++++ .../datawriter/DataWriterProvider.java | 60 ++ .../datawriter/FormattedDataWriter.java | 122 +++ .../datawriter/TemplateBasedDataWriter.java | 228 ++++++ .../droid/profile/CsvItemWriterTest.java | 384 +++++++++- .../droid/gui/DroidMainFrame.java | 4 + .../droid/gui/export/ExportAction.java | 29 +- .../droid/gui/export/ExportDialog.form | 702 +++++++++++------- .../droid/gui/export/ExportDialog.java | 311 ++++++-- .../droid/gui/util/SortedComboBoxModel.java | 54 ++ .../droid/gui/export/Bundle.properties | 2 + .../gui/util/SortedComboBoxModelTest.java | 87 +++ 43 files changed, 3724 insertions(+), 703 deletions(-) create mode 100644 droid-export-interfaces/src/main/java/uk/gov/nationalarchives/droid/export/interfaces/ExportDetails.java create mode 100644 droid-export-interfaces/src/main/java/uk/gov/nationalarchives/droid/export/interfaces/ExportTemplate.java create mode 100644 droid-export-interfaces/src/main/java/uk/gov/nationalarchives/droid/export/interfaces/ExportTemplateColumnDef.java create mode 100644 droid-export/src/main/java/uk/gov/nationalarchives/droid/export/template/ConstantStringColumnDef.java create mode 100644 droid-export/src/main/java/uk/gov/nationalarchives/droid/export/template/DataModifierColumnDef.java create mode 100644 droid-export/src/main/java/uk/gov/nationalarchives/droid/export/template/ExportTemplateBuilder.java create mode 100644 droid-export/src/main/java/uk/gov/nationalarchives/droid/export/template/ExportTemplateImpl.java create mode 100644 droid-export/src/main/java/uk/gov/nationalarchives/droid/export/template/ExportTemplateParseException.java create mode 100644 droid-export/src/main/java/uk/gov/nationalarchives/droid/export/template/ProfileResourceNodeColumnDef.java create mode 100644 droid-export/src/test/java/uk/gov/nationalarchives/droid/export/template/ConstantStringColumnDefTest.java create mode 100644 droid-export/src/test/java/uk/gov/nationalarchives/droid/export/template/DataModifierColumnDefTest.java create mode 100644 droid-export/src/test/java/uk/gov/nationalarchives/droid/export/template/ExportTemplateBuilderTest.java create mode 100644 droid-export/src/test/java/uk/gov/nationalarchives/droid/export/template/ProfileResourceNodeColumnDefTest.java create mode 100644 droid-help/src/main/resources/Images/Export dialog template.png create mode 100644 droid-results/src/main/java/uk/gov/nationalarchives/droid/profile/CsvWriterConstants.java create mode 100644 droid-results/src/main/java/uk/gov/nationalarchives/droid/profile/datawriter/ColumnBasedDataWriter.java create mode 100644 droid-results/src/main/java/uk/gov/nationalarchives/droid/profile/datawriter/DataWriterProvider.java create mode 100644 droid-results/src/main/java/uk/gov/nationalarchives/droid/profile/datawriter/FormattedDataWriter.java create mode 100644 droid-results/src/main/java/uk/gov/nationalarchives/droid/profile/datawriter/TemplateBasedDataWriter.java create mode 100644 droid-swing-ui/src/main/java/uk/gov/nationalarchives/droid/gui/util/SortedComboBoxModel.java create mode 100644 droid-swing-ui/src/test/java/uk/gov/nationalarchives/droid/gui/util/SortedComboBoxModelTest.java diff --git a/droid-command-line/src/main/java/uk/gov/nationalarchives/droid/command/action/CommandFactoryImpl.java b/droid-command-line/src/main/java/uk/gov/nationalarchives/droid/command/action/CommandFactoryImpl.java index def0293b0..af5c9cd10 100644 --- a/droid-command-line/src/main/java/uk/gov/nationalarchives/droid/command/action/CommandFactoryImpl.java +++ b/droid-command-line/src/main/java/uk/gov/nationalarchives/droid/command/action/CommandFactoryImpl.java @@ -115,6 +115,10 @@ public DroidCommand getExportFileCommand(final CommandLine cli) throws CommandLi cmd.setColumnsToWrite(columns); } + if (cli.hasOption(CommandLineParam.EXPORT_TEMPLATE.getLongName())) { + cmd.setExportTemplate(cli.getOptionValue(CommandLineParam.EXPORT_TEMPLATE.getLongName())); + } + if (cli.hasOption(CommandLineParam.ALL_FILTER.toString())) { cmd.setFilter(createFilter(cli.getOptionValues(CommandLineParam.ALL_FILTER.toString()), true)); } @@ -153,6 +157,10 @@ public DroidCommand getExportFormatCommand(final CommandLine cli) throws Command cmd.setColumnsToWrite(columns); } + if (cli.hasOption(CommandLineParam.EXPORT_TEMPLATE.getLongName())) { + cmd.setExportTemplate(cli.getOptionValue(CommandLineParam.EXPORT_TEMPLATE.getLongName())); + } + if (cli.hasOption(CommandLineParam.ALL_FILTER.toString())) { cmd.setFilter(createFilter(cli.getOptionValues(CommandLineParam.ALL_FILTER.toString()), true)); } diff --git a/droid-command-line/src/main/java/uk/gov/nationalarchives/droid/command/action/CommandLineParam.java b/droid-command-line/src/main/java/uk/gov/nationalarchives/droid/command/action/CommandLineParam.java index 6794ea2fd..89c51349e 100644 --- a/droid-command-line/src/main/java/uk/gov/nationalarchives/droid/command/action/CommandLineParam.java +++ b/droid-command-line/src/main/java/uk/gov/nationalarchives/droid/command/action/CommandLineParam.java @@ -237,6 +237,16 @@ public DroidCommand getCommand(CommandFactory commandFactory, CommandLine cli) { } }, + /** + * Specifies the absolute path for the export template to be used. + */ + EXPORT_TEMPLATE("et", "export-template", true, -1, I18N.EXPORT_TEMPLATE_HELP, "template-file") { + @Override + public DroidCommand getCommand(CommandFactory commandFactory, CommandLine cli) { + return null; + } + }, + /** * Specifies that a row per identification should be written out in a CSV file or console output. */ @@ -503,6 +513,7 @@ public static Options options() { options.addOptionGroup(getFilterOptionGroup()); options.addOptionGroup(getFileFilterOptionGroup()); + options.addOptionGroup(getExportOptionGroup()); options.addOptionGroup(topGroup); return options; @@ -523,6 +534,13 @@ private static OptionGroup getFilterOptionGroup() { return filterOptions; } + private static OptionGroup getExportOptionGroup() { + OptionGroup exportOptions = new OptionGroup(); + exportOptions.addOption(COLUMNS_TO_WRITE.newOption()); + exportOptions.addOption(EXPORT_TEMPLATE.newOption()); + return exportOptions; + } + /** * Single Options. * @@ -603,6 +621,7 @@ public static Options exportSubOptions() { options.addOption(BOM.newOption()); options.addOption(QUOTE_COMMAS.newOption()); options.addOption(COLUMNS_TO_WRITE.newOption()); + options.addOption(EXPORT_TEMPLATE.newOption()); return options; } diff --git a/droid-command-line/src/main/java/uk/gov/nationalarchives/droid/command/action/ExportCommand.java b/droid-command-line/src/main/java/uk/gov/nationalarchives/droid/command/action/ExportCommand.java index ac200e9a8..7ab872022 100644 --- a/droid-command-line/src/main/java/uk/gov/nationalarchives/droid/command/action/ExportCommand.java +++ b/droid-command-line/src/main/java/uk/gov/nationalarchives/droid/command/action/ExportCommand.java @@ -39,6 +39,7 @@ import java.util.concurrent.Future; import uk.gov.nationalarchives.droid.core.interfaces.filter.Filter; +import uk.gov.nationalarchives.droid.export.interfaces.ExportDetails; import uk.gov.nationalarchives.droid.export.interfaces.ExportManager; import uk.gov.nationalarchives.droid.export.interfaces.ExportOptions; import uk.gov.nationalarchives.droid.profile.ProfileInstance; @@ -60,7 +61,9 @@ public class ExportCommand implements DroidCommand { private boolean bom; private boolean quoteAllFields = true; private String columnsToWrite; - + + private String exportTemplate; + /** * {@inheritDoc} */ @@ -86,9 +89,7 @@ public void onProgress(Integer progress) { // Run the export try { //default to UTF-8 - final String outputEncoding = "UTF-8"; //TODO set encoding from command line option - final Future fProfiles = exportManager.exportProfiles(profileIds, destination, filter, - options, outputEncoding, bom, quoteAllFields, columnsToWrite); + final Future fProfiles = exportManager.exportProfiles(profileIds, destination, filter, getExportDetails()); fProfiles.get(); } catch (InterruptedException e) { throw new CommandExecutionException(e); @@ -218,4 +219,37 @@ public void setColumnsToWrite(String columnNames) { public String getColumnsToWrite() { return columnsToWrite; } + + /** + * @return Absolute path of export template. + */ + public String getExportTemplate() { + return exportTemplate; + } + + /** + * @param exportTemplate Absolute path of export template. + */ + public void setExportTemplate(String exportTemplate) { + this.exportTemplate = exportTemplate; + } + + + /** + * + * @return the export details for this export command. + * For an export from CLI, + * OutputEncoding is always defaulted to UTF-8 + */ + private ExportDetails getExportDetails() { + ExportDetails.ExportDetailsBuilder builder = new ExportDetails.ExportDetailsBuilder(); + + return builder.withExportOptions(getExportOptions()) + .withOutputEncoding("UTF-8") //default + .withBomFlag(isBom()) + .withQuotingAllFields(getQuoteAllFields()) + .withColumnsToWrite(getColumnsToWrite()) + .withExportTemplatePath(getExportTemplate()) + .build(); + } } diff --git a/droid-command-line/src/main/java/uk/gov/nationalarchives/droid/command/i18n/I18N.java b/droid-command-line/src/main/java/uk/gov/nationalarchives/droid/command/i18n/I18N.java index 61cfce787..456c0f50f 100644 --- a/droid-command-line/src/main/java/uk/gov/nationalarchives/droid/command/i18n/I18N.java +++ b/droid-command-line/src/main/java/uk/gov/nationalarchives/droid/command/i18n/I18N.java @@ -71,6 +71,11 @@ public final class I18N { */ public static final String COLUMNS_TO_WRITE_HELP = "profile.columnsToWrite.help"; + /** + * Sets the absolute path of an export template. + */ + public static final String EXPORT_TEMPLATE_HELP = "profile.exportTemplate.help"; + /** * Sets CSV output to write a row per format (rather than a row per file which is the default). */ @@ -221,5 +226,4 @@ public static String getResource(final String key, Object... params) { String pattern = getResource(key); return MessageFormat.format(pattern, params); } - } diff --git a/droid-command-line/src/main/resources/options.properties b/droid-command-line/src/main/resources/options.properties index 8fc34fcc9..5f0765329 100644 --- a/droid-command-line/src/main/resources/options.properties +++ b/droid-command-line/src/main/resources/options.properties @@ -92,7 +92,8 @@ filter.field.help=List the available fields to use in filters and the operators profile.run.help=Add resources to a new profile and run it. Resources are the file path of any file or folder you want to profile. The file paths should be given surrounded in double quotes, and separated by spaces from each other. The profile results will be saved to a single file specified using the -p option. \n For example: droid -a "C:\\Files\\A Folder" "C:\\Files\\file.xxx" -p "C:\\Results\\result1.droid" \n Note: You cannot use reporting, filtering and exporting when using the -a option. profile.outputfile.help=Outputs a profile as a CSV file to the path supplied. If "stdout" is specified, then output goes to the console. If no profile or output file is specified, then output defaults to the console. profile.quoteCommasOnly.help=Sets CSV output to only quote fields that have a comma in them. -profile.columnsToWrite.help=A space separated list of columns to write out in CSV output. Valid columns are:\nID PARENT_ID URI FILE_PATH NAME METHOD STATUS SIZE TYPE EXT LAST_MODIFIED EXTENSION_MISMATCH HASH FORMAT_COUNT PUID MIME_TYPE FORMAT_NAME FORMAT_VERSION +profile.columnsToWrite.help=[Optional] A space separated list of columns to write out in CSV output. Valid columns are:\nID PARENT_ID URI FILE_PATH NAME METHOD STATUS SIZE TYPE EXT LAST_MODIFIED EXTENSION_MISMATCH HASH FORMAT_COUNT PUID MIME_TYPE FORMAT_NAME FORMAT_VERSION. If omitted, all columns are exported. +profile.exportTemplate.help=[Optional] Absolute path to the export template file to be used for this export. If omitted, export falls back to -co option. profile.rowsPerFormat.help=Outputs a row per format for CSV, rather than a row per file which is the default. profile.run.file.help=Adds resources to a new profile which is outputted to a CSV file (or console). Resources are the file path of any file or folder you want to profile. The file paths should be given surrounded in double quotes, and separated by spaces from each other. The profile results will be saved to a single file specified using the -p option. \n For example: droid -Na "C:\\Files\\A Folder" "C:\\Files\\file.xxx" \n Note: You cannot use reporting, filtering and exporting when using the -Na option. no_profile.run.help=Identify either a specific file, or all files in a folder, without the use of a profile. The file or folder path should be bounded by double quotes. The scan results will be sent to standard output. \n For example: droid -Nr "C:\\Files\\A Folder" \n Note: You cannot use reporting, filtering and exporting when using the -Nr option. diff --git a/droid-command-line/src/test/java/uk/gov/nationalarchives/droid/command/action/ExportCommandTest.java b/droid-command-line/src/test/java/uk/gov/nationalarchives/droid/command/action/ExportCommandTest.java index 11f97c536..ef2241c7a 100644 --- a/droid-command-line/src/test/java/uk/gov/nationalarchives/droid/command/action/ExportCommandTest.java +++ b/droid-command-line/src/test/java/uk/gov/nationalarchives/droid/command/action/ExportCommandTest.java @@ -41,6 +41,7 @@ import uk.gov.nationalarchives.droid.core.interfaces.filter.CriterionOperator; import uk.gov.nationalarchives.droid.core.interfaces.filter.Filter; import uk.gov.nationalarchives.droid.core.interfaces.filter.FilterCriterion; +import uk.gov.nationalarchives.droid.export.interfaces.ExportDetails; import uk.gov.nationalarchives.droid.export.interfaces.ExportManager; import uk.gov.nationalarchives.droid.export.interfaces.ExportOptions; import uk.gov.nationalarchives.droid.profile.ProfileInstance; @@ -55,6 +56,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; @@ -92,7 +94,7 @@ public void testExportThreeProfiles() throws Exception { when(profileManager.open(eq(Paths.get("foo3")), any(ProgressObserver.class))).thenReturn(profile3); Future future = mock(Future.class); - when(exportManager.exportProfiles(any(List.class), eq(destination), (Filter) isNull(), eq(ExportOptions.ONE_ROW_PER_FORMAT), eq("UTF-8"), eq(false), eq(true), eq(null))).thenReturn(future); + when(exportManager.exportProfiles(any(List.class), eq(destination), (Filter) isNull(), any(ExportDetails.class))).thenReturn(future); ExportCommand command = new ExportCommand(); @@ -108,8 +110,18 @@ public void testExportThreeProfiles() throws Exception { String[] expectedExportedProfiles = new String[] { "profile1", "profile2", "profile3", }; - - verify(exportManager).exportProfiles(Arrays.asList(expectedExportedProfiles), destination, null, ExportOptions.ONE_ROW_PER_FORMAT, "UTF-8", false, true, null); + + ArgumentCaptor exportDetailsCaptor = ArgumentCaptor.forClass(ExportDetails.class); + ArgumentCaptor filterCaptor = ArgumentCaptor.forClass(Filter.class); + + verify(exportManager).exportProfiles(eq(Arrays.asList(expectedExportedProfiles)), eq(destination), filterCaptor.capture(), exportDetailsCaptor.capture()); + + ExportDetails details = exportDetailsCaptor.getValue(); + assertEquals(ExportOptions.ONE_ROW_PER_FORMAT, details.getExportOptions()); + assertEquals("UTF-8", details.getOutputEncoding()); + assertEquals(false, details.bomFlag()); + assertEquals(true, details.quoteAllFields()); + assertNull(details.getColumnsToWrite()); } @Test @@ -124,7 +136,7 @@ public void testExportProfileWithNarrowFilter() throws Exception { when(profileManager.open(eq(Paths.get("foo1")), any(ProgressObserver.class))).thenReturn(profile1); Future future = mock(Future.class); - when(exportManager.exportProfiles(any(List.class), eq("destination"), any(Filter.class), eq(ExportOptions.ONE_ROW_PER_FORMAT), any(String.class), eq(false), eq(true), eq(null))).thenReturn(future); + when(exportManager.exportProfiles(any(List.class), eq("destination"), any(Filter.class), any(ExportDetails.class))).thenReturn(future); ExportCommand command = new ExportCommand(); @@ -146,8 +158,15 @@ public void testExportProfileWithNarrowFilter() throws Exception { }; ArgumentCaptor filterCaptor = ArgumentCaptor.forClass(Filter.class); + ArgumentCaptor exportDetailsCaptor = ArgumentCaptor.forClass(ExportDetails.class); verify(exportManager).exportProfiles(eq(Arrays.asList(expectedExportedProfiles)), - eq("destination"), filterCaptor.capture(), eq(ExportOptions.ONE_ROW_PER_FORMAT), any(String.class), eq(false), eq(true), eq(null)); + eq("destination"), filterCaptor.capture(), exportDetailsCaptor.capture()); + + ExportDetails details = exportDetailsCaptor.getValue(); + assertEquals(ExportOptions.ONE_ROW_PER_FORMAT, details.getExportOptions()); + assertEquals(false, details.bomFlag()); + assertEquals(true, details.quoteAllFields()); + assertNull(details.getColumnsToWrite()); Filter filter = filterCaptor.getValue(); final List criteria = filter.getCriteria(); diff --git a/droid-core-interfaces/src/main/java/uk/gov/nationalarchives/droid/core/interfaces/config/DroidGlobalConfig.java b/droid-core-interfaces/src/main/java/uk/gov/nationalarchives/droid/core/interfaces/config/DroidGlobalConfig.java index f56e56ad0..19bce37f2 100644 --- a/droid-core-interfaces/src/main/java/uk/gov/nationalarchives/droid/core/interfaces/config/DroidGlobalConfig.java +++ b/droid-core-interfaces/src/main/java/uk/gov/nationalarchives/droid/core/interfaces/config/DroidGlobalConfig.java @@ -95,7 +95,9 @@ public class DroidGlobalConfig { private Path textSignatureFileDir; private Path reportDefinitionDir; private Path filterDir; - + + private Path exportTemplatesDir; + private PropertiesConfiguration props; private PropertiesConfiguration defaultProps; @@ -160,6 +162,9 @@ public DroidGlobalConfig() throws IOException { tempDir = Paths.get(droidTempPath, "tmp"); Files.createDirectories(tempDir); + + exportTemplatesDir = droidWorkDir.resolve("export_templates"); + Files.createDirectories(exportTemplatesDir); } /** @@ -357,7 +362,12 @@ public Path getFilterDir() { public Path getTempDir() { return tempDir; } - + + /** + * @return directory for the export templates. + */ + public Path getExportTemplatesDir() { return exportTemplatesDir; } + private void createResourceFile(final Path resourceDir, final String fileName, final String resourceName) throws IOException { final Path resourcefile = resourceDir.resolve(fileName); if (!Files.exists(resourcefile)) { diff --git a/droid-export-interfaces/src/main/java/uk/gov/nationalarchives/droid/export/interfaces/ExportDetails.java b/droid-export-interfaces/src/main/java/uk/gov/nationalarchives/droid/export/interfaces/ExportDetails.java new file mode 100644 index 000000000..f0cc9e6b7 --- /dev/null +++ b/droid-export-interfaces/src/main/java/uk/gov/nationalarchives/droid/export/interfaces/ExportDetails.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2016, The National Archives + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the The National Archives nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package uk.gov.nationalarchives.droid.export.interfaces; + +/** + * Wrapper class to wrap various options used for export. + * e.g. + * - whether the export is per file or per format + * - whether the data should be enclosed in quotes + * - whether BOM is to be used + * - Which columns to export + * - Is there an export template to be used + * + */ +public final class ExportDetails { + + private final ExportOptions exportOptions; + private final String outputEncoding; + private final boolean bomFlag; + private final boolean quoteAllFields; + private final String columnsToWrite; + private final String exportTemplatePath; + + /** + * Private constructor. The consumer can get the ExportDetails instance using ExportDetailsBuilder. + * @param exportOptions whether it is a "per row" export or "per format" export + * @param outputEncoding encoding to be used + * @param bomFlag whether to use BOM + * @param quoteAllFields whether the export fields should be enclosed in double quotes + * @param columnsToWrite List of columns to write + * @param exportTemplatePath absolute path to an export template, if one is being used. + */ + private ExportDetails(ExportOptions exportOptions, String outputEncoding, boolean bomFlag, boolean quoteAllFields, String columnsToWrite, String exportTemplatePath) { + this.exportOptions = exportOptions; + this.outputEncoding = outputEncoding; + this.bomFlag = bomFlag; + this.quoteAllFields = quoteAllFields; + this.columnsToWrite = columnsToWrite; + this.exportTemplatePath = exportTemplatePath; + } + + /** + * + * @return The export options. + */ + public ExportOptions getExportOptions() { + return exportOptions; + } + + /** + * @return The encoding for output. + */ + public String getOutputEncoding() { + return outputEncoding; + } + + /** + * + * @return status of bom. + */ + public boolean bomFlag() { + return bomFlag; + } + + /** + * @return whether all fields are quoted, or just those that contain field separators (commas). + */ + public boolean quoteAllFields() { + return quoteAllFields; + } + + /** + * @return A list of the columns to write, or null if all columns. + */ + public String getColumnsToWrite() { + return columnsToWrite; + } + + /** + * @return A path for export template, null if no template in use. + */ + public String getExportTemplatePath() { + return exportTemplatePath; + } + + /** + * Builder class to build the ExportDetails as a fluent API. + */ + public static class ExportDetailsBuilder { + private ExportOptions exportOptions = ExportOptions.ONE_ROW_PER_FILE; + private String outputEncoding = "UTF-8"; + private boolean bomFlag; + private boolean quoteAllFields = true; + private String columnsToWrite; + private String exportTemplatePath; + + public ExportDetailsBuilder withExportOptions(ExportOptions options) { + this.exportOptions = options; + return this; + } + + public ExportDetailsBuilder withOutputEncoding(String encoding) { + this.outputEncoding = encoding; + return this; + } + + public ExportDetailsBuilder withBomFlag(boolean bom) { + this.bomFlag = bom; + return this; + } + public ExportDetailsBuilder withQuotingAllFields(boolean quoteFields) { + this.quoteAllFields = quoteFields; + return this; + } + + public ExportDetailsBuilder withColumnsToWrite(String columns) { + this.columnsToWrite = columns; + return this; + } + + public ExportDetailsBuilder withExportTemplatePath(String templatePath) { + this.exportTemplatePath = templatePath; + return this; + } + + public ExportDetails build() { + return new ExportDetails(exportOptions, outputEncoding, bomFlag, quoteAllFields, columnsToWrite, exportTemplatePath); + } + } +} diff --git a/droid-export-interfaces/src/main/java/uk/gov/nationalarchives/droid/export/interfaces/ExportManager.java b/droid-export-interfaces/src/main/java/uk/gov/nationalarchives/droid/export/interfaces/ExportManager.java index e5cb6d582..f18032537 100644 --- a/droid-export-interfaces/src/main/java/uk/gov/nationalarchives/droid/export/interfaces/ExportManager.java +++ b/droid-export-interfaces/src/main/java/uk/gov/nationalarchives/droid/export/interfaces/ExportManager.java @@ -45,7 +45,6 @@ public interface ExportManager { /** * Exports one or more profiles to a CSV file. - * * FIXME: * The only reason this interface takes an optional filter * is so that the command line can pass in one of its own @@ -62,15 +61,10 @@ public interface ExportManager { * @param profileIds the list of profiles to export. * @param destination the destination filename * @param filter optional filter - * @param options the options for export. - * @param outputEncoding The character encoding to use in the output, null to use default encoding - * @param bom BOM flag. - * @param quoteAllFields - whether to quote all fields, or just those that contain commas. - * @param columnsToWrite a space separated list of column names to write. If null or empty, all columns are written. + * @param details details about various export preferences * @return future for cancelling the task. */ Future exportProfiles(List profileIds, String destination, - Filter filter, ExportOptions options, String outputEncoding, - boolean bom, boolean quoteAllFields, String columnsToWrite); + Filter filter, ExportDetails details); } diff --git a/droid-export-interfaces/src/main/java/uk/gov/nationalarchives/droid/export/interfaces/ExportTemplate.java b/droid-export-interfaces/src/main/java/uk/gov/nationalarchives/droid/export/interfaces/ExportTemplate.java new file mode 100644 index 000000000..e8a052d92 --- /dev/null +++ b/droid-export-interfaces/src/main/java/uk/gov/nationalarchives/droid/export/interfaces/ExportTemplate.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016, The National Archives + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the The National Archives nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package uk.gov.nationalarchives.droid.export.interfaces; + +import java.util.Map; + +/** + * ExportTemplate interface. + * There is only one method which returns a map of integer -> ExportTemplateColumnDef + */ +public interface ExportTemplate { + /** + * Get the map of Integer -> ExportTemplateColumnDef representing column order. + * @return Map with keys as column order and values as column definition + */ + Map getColumnOrderMap() ; + +} diff --git a/droid-export-interfaces/src/main/java/uk/gov/nationalarchives/droid/export/interfaces/ExportTemplateColumnDef.java b/droid-export-interfaces/src/main/java/uk/gov/nationalarchives/droid/export/interfaces/ExportTemplateColumnDef.java new file mode 100644 index 000000000..a4a66da53 --- /dev/null +++ b/droid-export-interfaces/src/main/java/uk/gov/nationalarchives/droid/export/interfaces/ExportTemplateColumnDef.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2016, The National Archives + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the The National Archives nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package uk.gov.nationalarchives.droid.export.interfaces; + +/** + * Export template column definition. + */ +public interface ExportTemplateColumnDef { + /** + * Returns the header label to be used in the output for this column. + * @return header + */ + String getHeaderLabel(); + + /** + * Returns the well-known column names from one of the default headers. + * Throws an exception if the definition represents a non-profile column + * @return origianl column name + * @throws RuntimeException Implementing methods may throw a RuntimeException if the method is not relevant + */ + String getOriginalColumnName(); + + /** + * Returns the data value, if any associated with this column definition. + * Throws an exception if the data is coming from profile results + * @return data value + * @throws RuntimeException Implementing methods may throw a RuntimeException if the method is not relevant + */ + String getDataValue(); + + /** + * Returns the column type for this column definition. + * @return column type + */ + ColumnType getColumnType(); + + /** + * Returns the result after performing the specific operation on the input. + * @param input String representing input data + * @return String after performing operation associated with this column definition. + */ + String getOperatedValue(String input); + + /** + * Column type as defined in the ExportTemplate. There are 3 types of columns. + */ + enum ColumnType { + /** + * ProfileResourceNode - The data comes directly from the profile result. + */ + ProfileResourceNode, + /** + * ConstantString - The data comes directly from the constant value in the template. + */ + ConstantString, + /** + * DataModifier - The data is modified as per the operation associated with column. + */ + DataModifier + } + + /** + * Data modification operations for the DataModifier columns type. + * At this time, only case change is supported + */ + enum DataModification { + /** + * Represents a data modification operation to convert given string value to lowercase. + */ + LCASE, + /** + * Represents a data modification operation to convert given string value to uppercase. + */ + UCASE + } +} diff --git a/droid-export-interfaces/src/main/java/uk/gov/nationalarchives/droid/export/interfaces/ItemWriter.java b/droid-export-interfaces/src/main/java/uk/gov/nationalarchives/droid/export/interfaces/ItemWriter.java index 5f8904107..1a06d2b35 100644 --- a/droid-export-interfaces/src/main/java/uk/gov/nationalarchives/droid/export/interfaces/ItemWriter.java +++ b/droid-export-interfaces/src/main/java/uk/gov/nationalarchives/droid/export/interfaces/ItemWriter.java @@ -88,4 +88,11 @@ public interface ItemWriter { */ void setColumnsToWrite(String columnNames); + /** + * Sets the ExportTemplate which can override the column names, column ordering and contents of the columns. + * + * @param template An instance of ExportTemplate. + */ + void setExportTemplate(ExportTemplate template); + } diff --git a/droid-export/src/main/java/uk/gov/nationalarchives/droid/export/ExportManagerImpl.java b/droid-export/src/main/java/uk/gov/nationalarchives/droid/export/ExportManagerImpl.java index 92352eff5..057c733ea 100644 --- a/droid-export/src/main/java/uk/gov/nationalarchives/droid/export/ExportManagerImpl.java +++ b/droid-export/src/main/java/uk/gov/nationalarchives/droid/export/ExportManagerImpl.java @@ -31,19 +31,20 @@ */ package uk.gov.nationalarchives.droid.export; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.FutureTask; - import uk.gov.nationalarchives.droid.core.interfaces.filter.Filter; +import uk.gov.nationalarchives.droid.export.interfaces.ExportDetails; import uk.gov.nationalarchives.droid.export.interfaces.ExportManager; -import uk.gov.nationalarchives.droid.export.interfaces.ExportOptions; import uk.gov.nationalarchives.droid.export.interfaces.ItemWriter; +import uk.gov.nationalarchives.droid.export.template.ExportTemplateBuilder; import uk.gov.nationalarchives.droid.profile.ProfileContextLocator; import uk.gov.nationalarchives.droid.profile.ProfileResourceNode; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; + /** * @author rflitcroft * @@ -74,13 +75,14 @@ public ExportManagerImpl(ProfileContextLocator profileContextLocator, } @Override - public Future exportProfiles(final List profileIds, final String destination, - final Filter filter, final ExportOptions options, final String outputEncoding, final boolean bom, - final boolean quoteAllFields, String columnsToWrite) { - itemWriter.setQuoteAllFields(quoteAllFields); - itemWriter.setColumnsToWrite(columnsToWrite); + public Future exportProfiles(final List profileIds, final String destination, + final Filter filter, final ExportDetails details + ) { + itemWriter.setQuoteAllFields(details.quoteAllFields()); + itemWriter.setColumnsToWrite(details.getColumnsToWrite()); + itemWriter.setExportTemplate(new ExportTemplateBuilder().buildExportTemplate(details.getExportTemplatePath())); final ExportTask exportTask = new ExportTask(destination, - profileIds, filter, options, outputEncoding, bom, itemWriter, profileContextLocator); + profileIds, filter, details.getExportOptions(), details.getOutputEncoding(), details.bomFlag(), itemWriter, profileContextLocator); final FutureTask task = new FutureTask(exportTask, null) { @Override public boolean cancel(final boolean mayInterruptIfRunning) { diff --git a/droid-export/src/main/java/uk/gov/nationalarchives/droid/export/template/ConstantStringColumnDef.java b/droid-export/src/main/java/uk/gov/nationalarchives/droid/export/template/ConstantStringColumnDef.java new file mode 100644 index 000000000..d39b2cad6 --- /dev/null +++ b/droid-export/src/main/java/uk/gov/nationalarchives/droid/export/template/ConstantStringColumnDef.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2016, The National Archives + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the The National Archives nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package uk.gov.nationalarchives.droid.export.template; + +import uk.gov.nationalarchives.droid.export.interfaces.ExportTemplateColumnDef; + +/** + * Class for a column definition representing constant string in the export template. + * e.g. foo: "bar" + * In such case, "foo" is the header for this column and "bar" is the value for it. + */ +public class ConstantStringColumnDef implements ExportTemplateColumnDef { + + private final String headerLabel; + private final String dataValue; + + public ConstantStringColumnDef(String dataValue, String headerLabel) { + this.dataValue = dataValue; + this.headerLabel = headerLabel; + } + + /** + * Returns the header label associated with this column definition. + * @return header label + */ + @Override + public String getHeaderLabel() { + return headerLabel; + } + + /** + * This type of column does not have a profile column associated with it. + * As a result, this method simply throws an exception if a consumer tries to get original column name + * @return nothing + */ + @Override + public String getOriginalColumnName() { + throw new RuntimeException("Constant String Columns do not have an associated original column name"); + } + + /** + * Returns the data value associated with this column as defined in the export template. + * @return data value + */ + @Override + public String getDataValue() { + return dataValue; + } + + @Override + public ColumnType getColumnType() { + return ColumnType.ConstantString; + } + + /** + * This type of column does not have any associated operation, hence returns the input value as it is. + * @param input String representing input data + * @return the input value as it is + */ + @Override + public String getOperatedValue(String input) { + return input == null ? "" : input; + } +} diff --git a/droid-export/src/main/java/uk/gov/nationalarchives/droid/export/template/DataModifierColumnDef.java b/droid-export/src/main/java/uk/gov/nationalarchives/droid/export/template/DataModifierColumnDef.java new file mode 100644 index 000000000..5c91da163 --- /dev/null +++ b/droid-export/src/main/java/uk/gov/nationalarchives/droid/export/template/DataModifierColumnDef.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2016, The National Archives + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the The National Archives nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package uk.gov.nationalarchives.droid.export.template; + +import uk.gov.nationalarchives.droid.export.interfaces.ExportTemplateColumnDef; + +/** + * Class for a column definition representing data modifier column in the export template. + * e.g. name: LCASE($FILE_PATH) + * In such case, "name" is the header for this column and the value is returned by performing + * an associated operation on the value retrieved from the underlying profile data. + * This type of column definition always has an underlying ProfileResourceNodeColumnDef + */ +public class DataModifierColumnDef implements ExportTemplateColumnDef { + + private final DataModification operation; + private final ProfileResourceNodeColumnDef innerDef; + + public DataModifierColumnDef(ProfileResourceNodeColumnDef innerDef, DataModification operation) { + this.innerDef = innerDef; + this.operation = operation; + } + + /** + * Returns the header label associated with this column definition. + * @return header label + */ + @Override + public String getHeaderLabel() { + return innerDef.getHeaderLabel(); + } + + /** + * Returns the original column name from an underlying definition. + * @return original name of underlying column. + */ + @Override + public String getOriginalColumnName() { + return innerDef.getOriginalColumnName(); + } + + /** + * Return data value from the underlying definition. + * @return data value from underlying definition + */ + @Override + public String getDataValue() { + return innerDef.getDataValue(); + } + + @Override + public ColumnType getColumnType() { + return ColumnType.DataModifier; + } + + /** + * Returns a value after performing an associated operation on the input value. + * e.g. If the operation is LCASE, it will convert the supplied value to lowercase + * @param input String representing input data + * @return String value after performing associated operation. + * Note: + * We only support LCASE and UCASE at the moment, if more operations need to be supported. + * they can be defined in the DataModification enum and appropriate conversion can be + * implemented here. We only support an operation returning String by taking in a single + * String parameter. + * + */ + @Override + public String getOperatedValue(String input) { + if ((input == null) || (input.isEmpty())) { + return ""; + } + switch(operation) { + case LCASE: + return input.toLowerCase(); + case UCASE: + return input.toUpperCase(); + default: + throw new RuntimeException("Value conversion for operation " + operation + " is not implemented"); + } + } +} diff --git a/droid-export/src/main/java/uk/gov/nationalarchives/droid/export/template/ExportTemplateBuilder.java b/droid-export/src/main/java/uk/gov/nationalarchives/droid/export/template/ExportTemplateBuilder.java new file mode 100644 index 000000000..88f0c58e6 --- /dev/null +++ b/droid-export/src/main/java/uk/gov/nationalarchives/droid/export/template/ExportTemplateBuilder.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2016, The National Archives + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the The National Archives nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package uk.gov.nationalarchives.droid.export.template; + +import uk.gov.nationalarchives.droid.export.interfaces.ExportTemplate; +import uk.gov.nationalarchives.droid.export.interfaces.ExportTemplateColumnDef; +import uk.gov.nationalarchives.droid.profile.CsvWriterConstants; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Class to build export template from a file. + * The class parses a file in a simple way using delimiters and string manipulations. + */ +public class ExportTemplateBuilder { + + private static final String COLON = ":"; + private static final String UNABLE_TO_PARSE_LINE_MSG = "Unable to parse line: '%s', %s"; + private static final String INVALID_DATA_MODIFIER_SYNTAX_MESSAGE_FORMAT = "Invalid syntax in data modifier expression '%s', %s"; + private static final String DOUBLE_QUOTES = "\""; + private static final String OPENING_BRACKET = "("; + private static final String CLOSING_BRACKET = ")"; + private static final String DATA_COLUMN_PREFIX = "$"; + + /** + * The entry point into building an ExportTemplate object by reading a template file. + * @param pathToTemplate absolute path to the template file + * @return ExportTemplate object + */ + public ExportTemplate buildExportTemplate(String pathToTemplate) { + if (pathToTemplate == null) { + return null; + } + + List templateLines; + try { + templateLines = Files.readAllLines(Paths.get(pathToTemplate)); + Map columnMap = buildColumnMap(templateLines); + return new ExportTemplateImpl(columnMap); + } catch (IOException e) { + throw new RuntimeException("Unable to read export template file at path: " + pathToTemplate); + } + } + + private Map buildColumnMap(List templateLines) { + if ((templateLines == null) || (templateLines.size() == 0)) { + throw new ExportTemplateParseException("Export template is empty"); + } + String versionLine = templateLines.get(0); + String version = parseVersionLine(versionLine); + + //we have only one version at the moment, but future provision for versioning + switch (version) { + case "1.0": + return parseExportTemplateV1(templateLines.subList(1, templateLines.size())); + default: + throw new ExportTemplateParseException("Unsupported version for the export template"); + } + } + + private Map parseExportTemplateV1(List templateLines) { + List columnLines = templateLines.stream().filter(line -> line.trim().length() > 0).collect(Collectors.toList()); + Map columnMap = new HashMap<>(); + for (int i = 0; i < columnLines.size(); i++) { + String line = columnLines.get(i); + if (!line.contains(COLON)) { + throw new ExportTemplateParseException(String.format(UNABLE_TO_PARSE_LINE_MSG, line, "line does not contain ':'")); + } + String header = line.substring(0, line.indexOf(COLON)).trim(); + if (header.isEmpty()) { + throw new ExportTemplateParseException(String.format(UNABLE_TO_PARSE_LINE_MSG, line, "column header is empty")); + } + + String token2 = line.substring(line.indexOf(COLON) + 1).trim(); + + if (token2.startsWith(DATA_COLUMN_PREFIX)) { + columnMap.put(i, createProfileNodeDef(header, token2)); + } else if ((token2.isEmpty()) || (token2.startsWith(DOUBLE_QUOTES))) { + columnMap.put(i, createConstantStringDef(header, token2)); + } else { + columnMap.put(i, createDataModifierDef(header, token2)); + } + } + return columnMap; + } + + private ExportTemplateColumnDef createDataModifierDef(String header, String param2) { + + assertDataModifierSyntaxValid(param2); + + String[] tokens = param2.split("\\("); + String operationName = tokens[0].trim(); + + List operations = Arrays.stream( + ExportTemplateColumnDef.DataModification.values()).map(v -> v.toString()). + collect(Collectors.toList()); + + if (!operations.contains(operationName)) { + throw new ExportTemplateParseException("Undefined operation '" + operationName + "' encountered in export template"); + } + + String column = tokens[1].trim().substring(0, tokens[1].trim().length() - 1); + ProfileResourceNodeColumnDef inner = createProfileNodeDef(header, column); + + ExportTemplateColumnDef.DataModification operation = ExportTemplateColumnDef.DataModification.valueOf(operationName); + return new DataModifierColumnDef(inner, operation); + } + + private void assertDataModifierSyntaxValid(String expression) { + String expressionToTest = expression.trim(); + // valid statement like LCASE($URI) + if (expressionToTest.chars().filter(ch -> ch == '(').count() != 1) { + throw new ExportTemplateParseException(String.format(INVALID_DATA_MODIFIER_SYNTAX_MESSAGE_FORMAT, expression, "expecting exactly one occurrence of '('")); + } + if (expressionToTest.chars().filter(ch -> ch == ')').count() != 1) { + throw new ExportTemplateParseException(String.format(INVALID_DATA_MODIFIER_SYNTAX_MESSAGE_FORMAT, expression, "expecting exactly one occurrence of ')'")); + } + if (expressionToTest.indexOf(OPENING_BRACKET) > expressionToTest.indexOf(CLOSING_BRACKET)) { + throw new ExportTemplateParseException(String.format(INVALID_DATA_MODIFIER_SYNTAX_MESSAGE_FORMAT, expression, "expecting '(' before ')'")); + } + if (expressionToTest.indexOf(OPENING_BRACKET) == 0) { + throw new ExportTemplateParseException(String.format(INVALID_DATA_MODIFIER_SYNTAX_MESSAGE_FORMAT, expression, "expecting an operation definition before '('")); + } + String dataColumnToken = expressionToTest.substring(expressionToTest.indexOf(OPENING_BRACKET) + 1, + expressionToTest.indexOf(CLOSING_BRACKET)).trim(); + if (!dataColumnToken.startsWith(DATA_COLUMN_PREFIX)) { + throw new ExportTemplateParseException(String.format(INVALID_DATA_MODIFIER_SYNTAX_MESSAGE_FORMAT, expression, "expecting '$' after '('")); + } + } + + private ExportTemplateColumnDef createConstantStringDef(String header, String param2) { + if (param2.isEmpty()) { + return new ConstantStringColumnDef("", header); + } else { + if (!param2.endsWith(DOUBLE_QUOTES)) { + throw new ExportTemplateParseException("The line with a constant value ('" + param2 + "') in template definition does not have closing quotes"); + } + return new ConstantStringColumnDef(param2.substring(1, param2.length() - 1), header); + } + } + + private ProfileResourceNodeColumnDef createProfileNodeDef(String header, String param2) { + String messageFormat = "Invalid column name. '%s' does not exist in profile results"; + if (!param2.startsWith(DATA_COLUMN_PREFIX)) { + throw new ExportTemplateParseException(String.format(messageFormat, param2)); + } + String originalColumnName = param2.substring(1); + if (!(Arrays.stream(CsvWriterConstants.HEADERS).collect(Collectors.toList()).contains(originalColumnName))) { + throw new ExportTemplateParseException(String.format(messageFormat, originalColumnName)); + } + return new ProfileResourceNodeColumnDef(originalColumnName, header); + } + + private String parseVersionLine(String versionLine) { + String versionPrefix = "version"; + String versionString = versionLine.trim(); + if (!versionString.startsWith(versionPrefix)) { + throw new ExportTemplateParseException("First line in the template needs to specify version in the form \"version \""); + } + + return versionString.substring(versionPrefix.length()).trim(); + } +} + + diff --git a/droid-export/src/main/java/uk/gov/nationalarchives/droid/export/template/ExportTemplateImpl.java b/droid-export/src/main/java/uk/gov/nationalarchives/droid/export/template/ExportTemplateImpl.java new file mode 100644 index 000000000..ff9cac07e --- /dev/null +++ b/droid-export/src/main/java/uk/gov/nationalarchives/droid/export/template/ExportTemplateImpl.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2016, The National Archives + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the The National Archives nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package uk.gov.nationalarchives.droid.export.template; + +import uk.gov.nationalarchives.droid.export.interfaces.ExportTemplate; +import uk.gov.nationalarchives.droid.export.interfaces.ExportTemplateColumnDef; + +import java.util.HashMap; +import java.util.Map; + +/** + * Implementation of ExportTemplate. + */ +public class ExportTemplateImpl implements ExportTemplate { + private final Map columnOrderMap = new HashMap<>(); + + public ExportTemplateImpl(final Map columnOrder) { + this.columnOrderMap.putAll(columnOrder); + } + public Map getColumnOrderMap() { + return columnOrderMap; + } +} diff --git a/droid-export/src/main/java/uk/gov/nationalarchives/droid/export/template/ExportTemplateParseException.java b/droid-export/src/main/java/uk/gov/nationalarchives/droid/export/template/ExportTemplateParseException.java new file mode 100644 index 000000000..ada883f55 --- /dev/null +++ b/droid-export/src/main/java/uk/gov/nationalarchives/droid/export/template/ExportTemplateParseException.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016, The National Archives + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the The National Archives nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package uk.gov.nationalarchives.droid.export.template; + + +public class ExportTemplateParseException extends RuntimeException { + public ExportTemplateParseException(String message) { + super(message); + } + + public ExportTemplateParseException() { + this("Unable to parse export template file"); + } +} diff --git a/droid-export/src/main/java/uk/gov/nationalarchives/droid/export/template/ProfileResourceNodeColumnDef.java b/droid-export/src/main/java/uk/gov/nationalarchives/droid/export/template/ProfileResourceNodeColumnDef.java new file mode 100644 index 000000000..2de4a7986 --- /dev/null +++ b/droid-export/src/main/java/uk/gov/nationalarchives/droid/export/template/ProfileResourceNodeColumnDef.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2016, The National Archives + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the The National Archives nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package uk.gov.nationalarchives.droid.export.template; + +import uk.gov.nationalarchives.droid.export.interfaces.ExportTemplateColumnDef; + +/** + * Class for a column definition representing an underlying profile resource node column. + * e.g. identifier: $ID + * In such case, "identifier" is the header for this column and the data is retrieved from the + * ID column in the results . + */ +public class ProfileResourceNodeColumnDef implements ExportTemplateColumnDef { + + private final String originalHeaderLabel; + private final String headerLabel; + + public ProfileResourceNodeColumnDef(String originalHeaderLabel, String headerLabel) { + this.originalHeaderLabel = originalHeaderLabel; + this.headerLabel = headerLabel; + } + + /** + * Returns the header label associated with this column definition. + * @return header label + */ + @Override + public String getHeaderLabel() { + return headerLabel; + } + + /** + * Returns the original column name. + * @return The well-known name of column as it appears in the profile results + */ + @Override + public String getOriginalColumnName() { + return originalHeaderLabel; + } + + /** + * This type of column does not have data associated with it. + * As a result, this method simply throws an exception if a consumer tries to get data from it + * @return nothing + */ + @Override + public String getDataValue() { + throw new RuntimeException("Profile resource node column uses data from the profile results"); + } + + @Override + public ColumnType getColumnType() { + return ColumnType.ProfileResourceNode; + } + + /** + * This type of column does not have any associated operation, hence returns the input value as it is. + * @param input String representing input data + * @return the input value as it is + */ + @Override + public String getOperatedValue(String input) { + return input == null ? "" : input; + } +} diff --git a/droid-export/src/test/java/uk/gov/nationalarchives/droid/export/ExportJobIntegrationTest.java b/droid-export/src/test/java/uk/gov/nationalarchives/droid/export/ExportJobIntegrationTest.java index 14e363d16..fdb5b757d 100644 --- a/droid-export/src/test/java/uk/gov/nationalarchives/droid/export/ExportJobIntegrationTest.java +++ b/droid-export/src/test/java/uk/gov/nationalarchives/droid/export/ExportJobIntegrationTest.java @@ -45,6 +45,7 @@ import org.springframework.test.context.ContextConfiguration; import uk.gov.nationalarchives.droid.core.interfaces.config.DroidGlobalConfig; +import uk.gov.nationalarchives.droid.export.interfaces.ExportDetails; import uk.gov.nationalarchives.droid.export.interfaces.ExportOptions; import uk.gov.nationalarchives.droid.profile.DirectoryProfileResource; import uk.gov.nationalarchives.droid.profile.ProfileContextLocator; @@ -98,10 +99,14 @@ public void testEndToEndExportOfOneProfile() throws Exception { String[] profileIds = new String[] { "test", }; - - exportManager.exportProfiles(Arrays.asList(profileIds), "exports/export.csv", null, - ExportOptions.ONE_ROW_PER_FILE, null, false, true, null); - + + ExportDetails details = new ExportDetails.ExportDetailsBuilder() + .withExportOptions(ExportOptions.ONE_ROW_PER_FILE) + .withQuotingAllFields(true) + .build(); + + exportManager.exportProfiles(Arrays.asList(profileIds), "exports/export.csv", null, details); + profileContextLocator.removeProfileContext("test"); } diff --git a/droid-export/src/test/java/uk/gov/nationalarchives/droid/export/ExportManagerImplTest.java b/droid-export/src/test/java/uk/gov/nationalarchives/droid/export/ExportManagerImplTest.java index 70b08b250..fd5f10cf5 100644 --- a/droid-export/src/test/java/uk/gov/nationalarchives/droid/export/ExportManagerImplTest.java +++ b/droid-export/src/test/java/uk/gov/nationalarchives/droid/export/ExportManagerImplTest.java @@ -31,34 +31,36 @@ */ package uk.gov.nationalarchives.droid.export; +import org.junit.Before; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import uk.gov.nationalarchives.droid.core.interfaces.filter.Filter; +import uk.gov.nationalarchives.droid.export.interfaces.ExportDetails; +import uk.gov.nationalarchives.droid.export.interfaces.ExportOptions; +import uk.gov.nationalarchives.droid.export.interfaces.ItemReader; +import uk.gov.nationalarchives.droid.export.interfaces.ItemReaderCallback; +import uk.gov.nationalarchives.droid.export.interfaces.ItemWriter; +import uk.gov.nationalarchives.droid.export.interfaces.JobCancellationException; +import uk.gov.nationalarchives.droid.profile.ProfileContextLocator; +import uk.gov.nationalarchives.droid.profile.ProfileInstance; +import uk.gov.nationalarchives.droid.profile.ProfileInstanceManager; +import uk.gov.nationalarchives.droid.profile.ProfileResourceNode; + import java.io.Writer; import java.net.URI; import java.util.ArrayList; import java.util.List; import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.junit.MockitoJUnitRunner; -import org.mockito.stubbing.Answer; - -import uk.gov.nationalarchives.droid.core.interfaces.filter.Filter; -import uk.gov.nationalarchives.droid.export.interfaces.*; -import uk.gov.nationalarchives.droid.profile.ProfileContextLocator; -import uk.gov.nationalarchives.droid.profile.ProfileInstance; -import uk.gov.nationalarchives.droid.profile.ProfileInstanceManager; -import uk.gov.nationalarchives.droid.profile.ProfileResourceNode; - /** * @author rflitcroft * @@ -125,8 +127,14 @@ public Object answer(InvocationOnMock invocation) { List profileIdList = new ArrayList(); profileIdList.add("profile1"); - - exportManager.exportProfiles(profileIdList, "destination", null, ExportOptions.ONE_ROW_PER_FILE, null, false, true, "").get(); + + ExportDetails details = new ExportDetails.ExportDetailsBuilder() + .withExportOptions(ExportOptions.ONE_ROW_PER_FILE) + .withQuotingAllFields(true) + .withColumnsToWrite("") + .build(); + + exportManager.exportProfiles(profileIdList, "destination", null, details).get(); verify(writer).open(any(Writer.class)); verify(writer).write(fis); diff --git a/droid-export/src/test/java/uk/gov/nationalarchives/droid/export/template/ConstantStringColumnDefTest.java b/droid-export/src/test/java/uk/gov/nationalarchives/droid/export/template/ConstantStringColumnDefTest.java new file mode 100644 index 000000000..555c456c0 --- /dev/null +++ b/droid-export/src/test/java/uk/gov/nationalarchives/droid/export/template/ConstantStringColumnDefTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2016, The National Archives + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the The National Archives nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package uk.gov.nationalarchives.droid.export.template; + +import org.junit.Test; +import uk.gov.nationalarchives.droid.export.interfaces.ExportTemplateColumnDef; + +import static org.junit.Assert.*; + +public class ConstantStringColumnDefTest { + @Test + public void should_operate_on_null_value_and_return_empty_string() { + ConstantStringColumnDef def = new ConstantStringColumnDef("English (UK)", "Language"); + assertEquals("", def.getOperatedValue(null)); + } + + @Test + public void should_return_input_as_it_is_when_asked_for_operated_value() { + ConstantStringColumnDef def = new ConstantStringColumnDef("English (UK)", "Language"); + assertEquals("FoRMat", def.getOperatedValue("FoRMat")); + } + + @Test + public void should_return_header_label_as_set_when_defining_the_column_definition() { + ConstantStringColumnDef def = new ConstantStringColumnDef("English (UK)", "Language"); + assertEquals("Language", def.getHeaderLabel()); + } + @Test + public void should_throw_exception_when_asking_for_original_column_name() { + ConstantStringColumnDef def = new ConstantStringColumnDef("English (UK)", "Language"); + RuntimeException ex = assertThrows(RuntimeException.class, () -> def.getOriginalColumnName()); + assertEquals("Constant String Columns do not have an associated original column name", ex.getMessage()); + } + + @Test + public void should_return_data_as_set_in_the_definition() { + ConstantStringColumnDef def = new ConstantStringColumnDef("English (UK)", "Language"); + assertEquals("English (UK)", def.getDataValue()); + } + + @Test + public void should_return_column_type_as_data_modifier_column_def() { + ConstantStringColumnDef def = new ConstantStringColumnDef("English (UK)", "Language"); + assertEquals(ExportTemplateColumnDef.ColumnType.ConstantString, def.getColumnType()); + } +} \ No newline at end of file diff --git a/droid-export/src/test/java/uk/gov/nationalarchives/droid/export/template/DataModifierColumnDefTest.java b/droid-export/src/test/java/uk/gov/nationalarchives/droid/export/template/DataModifierColumnDefTest.java new file mode 100644 index 000000000..028642d1d --- /dev/null +++ b/droid-export/src/test/java/uk/gov/nationalarchives/droid/export/template/DataModifierColumnDefTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2016, The National Archives + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the The National Archives nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package uk.gov.nationalarchives.droid.export.template; + +import org.junit.Test; +import uk.gov.nationalarchives.droid.export.interfaces.ExportTemplateColumnDef; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class DataModifierColumnDefTest { + @Test + public void should_operate_on_null_value_and_return_empty_string() { + ProfileResourceNodeColumnDef innerDef = mock(ProfileResourceNodeColumnDef.class); + DataModifierColumnDef def = new DataModifierColumnDef(innerDef, ExportTemplateColumnDef.DataModification.LCASE); + assertEquals("", def.getOperatedValue(null)); + } + + @Test + public void should_return_header_label_as_set_on_the_inner_profile_def() { + ProfileResourceNodeColumnDef innerDef = mock(ProfileResourceNodeColumnDef.class); + when(innerDef.getHeaderLabel()).thenReturn("InnerHeader"); + DataModifierColumnDef def = new DataModifierColumnDef(innerDef, ExportTemplateColumnDef.DataModification.LCASE); + assertEquals("InnerHeader", def.getHeaderLabel()); + } + @Test + public void should_return_original_column_name_as_set_on_the_inner_profile_def() { + ProfileResourceNodeColumnDef innerDef = mock(ProfileResourceNodeColumnDef.class); + when(innerDef.getOriginalColumnName()).thenReturn("FORMAT_NAME"); + DataModifierColumnDef def = new DataModifierColumnDef(innerDef, ExportTemplateColumnDef.DataModification.LCASE); + assertEquals("FORMAT_NAME", def.getOriginalColumnName()); + } + + @Test + public void should_return_column_type_as_data_modifier_column_def() { + ProfileResourceNodeColumnDef innerDef = mock(ProfileResourceNodeColumnDef.class); + DataModifierColumnDef def = new DataModifierColumnDef(innerDef, ExportTemplateColumnDef.DataModification.LCASE); + assertEquals(ExportTemplateColumnDef.ColumnType.DataModifier, def.getColumnType()); + } +} \ No newline at end of file diff --git a/droid-export/src/test/java/uk/gov/nationalarchives/droid/export/template/ExportTemplateBuilderTest.java b/droid-export/src/test/java/uk/gov/nationalarchives/droid/export/template/ExportTemplateBuilderTest.java new file mode 100644 index 000000000..4a81d0836 --- /dev/null +++ b/droid-export/src/test/java/uk/gov/nationalarchives/droid/export/template/ExportTemplateBuilderTest.java @@ -0,0 +1,286 @@ +/* + * Copyright (c) 2016, The National Archives + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the The National Archives nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package uk.gov.nationalarchives.droid.export.template; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import uk.gov.nationalarchives.droid.export.interfaces.ExportTemplate; +import uk.gov.nationalarchives.droid.export.interfaces.ExportTemplateColumnDef; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.*; + +public class ExportTemplateBuilderTest { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void should_read_an_export_template_file_and_construct_export_template_object() throws IOException { + ExportTemplateBuilder builder = new ExportTemplateBuilder(); + File tempFile = temporaryFolder.newFile("export-task-test-default-encoding"); + List data = Arrays.asList( + "version 1.0", + "Identifier: $ID", + "Language: \"Gibberish\"", + "Path: UCASE($FILE_PATH)", + "Size: $SIZE", + "HASH: $HASH"); + Files.write(tempFile.toPath(), data, StandardOpenOption.WRITE); + ExportTemplate template = builder.buildExportTemplate(tempFile.getAbsolutePath()); + assertNotNull(template); + ExportTemplateColumnDef cscd = template.getColumnOrderMap().get(1); + assertTrue(cscd instanceof ConstantStringColumnDef); + assertEquals("Gibberish",template.getColumnOrderMap().get(1).getDataValue()); + + ExportTemplateColumnDef dmcd = template.getColumnOrderMap().get(2); + assertTrue(dmcd instanceof DataModifierColumnDef); + assertEquals("FILE_PATH", dmcd.getOriginalColumnName()); + assertEquals("SMALL", dmcd.getOperatedValue("small")); + } + + @Test + public void should_throw_exception_when_the_column_description_lines_do_not_have_a_colon() throws IOException { + File tempFile = temporaryFolder.newFile("export-task-test-default-encoding"); + List data = Arrays.asList("version 1.0", "myCol $My_COL"); + Files.write(tempFile.toPath(), data, StandardOpenOption.WRITE); + ExportTemplateBuilder builder = new ExportTemplateBuilder(); + ExportTemplateParseException ex = assertThrows(ExportTemplateParseException.class, () -> builder.buildExportTemplate(tempFile.getAbsolutePath())); + assertEquals("Unable to parse line: 'myCol $My_COL', line does not contain ':'", ex.getMessage()); + } + + @Test + public void should_throw_exception_when_the_header_information_is_missing() throws IOException { + File tempFile = temporaryFolder.newFile("export-task-test-default-encoding"); + List data = Arrays.asList("version 1.0", ": $My_COL"); + Files.write(tempFile.toPath(), data, StandardOpenOption.WRITE); + ExportTemplateBuilder builder = new ExportTemplateBuilder(); + ExportTemplateParseException ex = assertThrows(ExportTemplateParseException.class, () -> builder.buildExportTemplate(tempFile.getAbsolutePath())); + assertEquals("Unable to parse line: ': $My_COL', column header is empty", ex.getMessage()); + } + + @Test + public void should_treat_missing_value_as_empty_string_constant_in_export_template() throws IOException { + ExportTemplateBuilder builder = new ExportTemplateBuilder(); + File tempFile = temporaryFolder.newFile("export-task-test-default-encoding"); + List data = Arrays.asList( + "version 1.0", + "Identifier: $ID", + "Language: ", + "Path: UCASE($FILE_PATH)"); + Files.write(tempFile.toPath(), data, StandardOpenOption.WRITE); + ExportTemplate template = builder.buildExportTemplate(tempFile.getAbsolutePath()); + assertNotNull(template); + assertTrue(template.getColumnOrderMap().get(1) instanceof ConstantStringColumnDef); + assertTrue(template.getColumnOrderMap().get(2) instanceof DataModifierColumnDef); + assertEquals("FILE_PATH", template.getColumnOrderMap().get(2).getOriginalColumnName()); + } + + @Test + public void should_throw_exception_when_version_string_is_bad() throws IOException { + File tempFile = temporaryFolder.newFile("export-task-test-default-encoding"); + Files.write(tempFile.toPath(), "versio 1.3".getBytes(StandardCharsets.UTF_8)); + List data = Collections.singletonList("myCol:$My_COL"); + Files.write(tempFile.toPath(), data, StandardOpenOption.APPEND); + ExportTemplateBuilder builder = new ExportTemplateBuilder(); + ExportTemplateParseException ex = assertThrows(ExportTemplateParseException.class, () -> builder.buildExportTemplate(tempFile.getAbsolutePath())); + assertEquals("First line in the template needs to specify version in the form \"version \"", ex.getMessage()); + } + + @Test + public void should_trim_blanks_from_lines_and_tokens_to_produce_a_valid_template() throws IOException { + ExportTemplateBuilder builder = new ExportTemplateBuilder(); + File tempFile = temporaryFolder.newFile("export-task-test-default-encoding"); + List data = Arrays.asList( + "version 1.0", + "", + "Language: \"Marathi\" ", + ""); + Files.write(tempFile.toPath(), data, StandardOpenOption.WRITE); + ExportTemplate template = builder.buildExportTemplate(tempFile.getAbsolutePath()); + assertEquals(1, template.getColumnOrderMap().size()); + assertEquals("Marathi", template.getColumnOrderMap().get(0).getDataValue()); + } + + @Test + public void should_allow_constant_strings_with_double_quotes_in_the_data_value() throws IOException { + ExportTemplateBuilder builder = new ExportTemplateBuilder(); + File tempFile = temporaryFolder.newFile("export-task-test-default-encoding"); + List data = Arrays.asList( + "version 1.0", + "", + "Language: \"Star trek: \"Klingon\"\"", + ""); + Files.write(tempFile.toPath(), data, StandardOpenOption.WRITE); + ExportTemplate template = builder.buildExportTemplate(tempFile.getAbsolutePath()); + assertEquals(1, template.getColumnOrderMap().size()); + assertEquals("Star trek: \"Klingon\"", template.getColumnOrderMap().get(0).getDataValue()); + } + + @Test + public void should_throw_an_exception_if_the_constant_string_value_does_not_have_closing_double_quotes() throws IOException { + ExportTemplateBuilder builder = new ExportTemplateBuilder(); + File tempFile = temporaryFolder.newFile("export-task-test-default-encoding"); + List data = Arrays.asList( + "version 1.0", + "", + "Language: \"English", + ""); + Files.write(tempFile.toPath(), data, StandardOpenOption.WRITE); + + ExportTemplateParseException ex = assertThrows(ExportTemplateParseException.class, () -> builder.buildExportTemplate(tempFile.getAbsolutePath())); + assertEquals("The line with a constant value ('\"English') in template definition does not have closing quotes", ex.getMessage()); + } + + @Test + public void should_throw_an_exception_if_the_column_name_given_for_profile_resource_column_is_not_a_well_known_header() throws IOException { + ExportTemplateBuilder builder = new ExportTemplateBuilder(); + File tempFile = temporaryFolder.newFile("export-task-test-default-encoding"); + List data = Arrays.asList( + "version 1.0", + "", + "Identifier: $UUID", + ""); + Files.write(tempFile.toPath(), data, StandardOpenOption.WRITE); + + ExportTemplateParseException ex = assertThrows(ExportTemplateParseException.class, () -> builder.buildExportTemplate(tempFile.getAbsolutePath())); + assertEquals("Invalid column name. 'UUID' does not exist in profile results", ex.getMessage()); + } + + @Test + public void should_throw_an_exception_when_the_operation_is_unknown() throws IOException { + ExportTemplateBuilder builder = new ExportTemplateBuilder(); + File tempFile = temporaryFolder.newFile("export-task-test-default-encoding"); + List data = Arrays.asList( + "version 1.0", + "", + "Identifier: Lower($ID)", + ""); + Files.write(tempFile.toPath(), data, StandardOpenOption.WRITE); + + ExportTemplateParseException ex = assertThrows(ExportTemplateParseException.class, () -> builder.buildExportTemplate(tempFile.getAbsolutePath())); + assertEquals("Undefined operation 'Lower' encountered in export template", ex.getMessage()); + } + + @Test + public void should_trim_leading_and_trailing_spaces_from_a_header_label() throws IOException { + ExportTemplateBuilder builder = new ExportTemplateBuilder(); + File tempFile = temporaryFolder.newFile("export-task-test-default-encoding"); + List data = Arrays.asList( + "version 1.0", + " Identifier : \"Something\"", + ""); + Files.write(tempFile.toPath(), data, StandardOpenOption.WRITE); + + ExportTemplate template = builder.buildExportTemplate(tempFile.getAbsolutePath()); + assertEquals("Identifier", template.getColumnOrderMap().get(0).getHeaderLabel()); + } + + @Test + public void should_support_colon_in_the_constant_string_value() throws IOException { + ExportTemplateBuilder builder = new ExportTemplateBuilder(); + File tempFile = temporaryFolder.newFile("export-task-test-default-encoding"); + List data = Arrays.asList( + "version 1.0", + " MyWebsite : \"http://www.knowingwhere.com\"", + ""); + Files.write(tempFile.toPath(), data, StandardOpenOption.WRITE); + + ExportTemplate template = builder.buildExportTemplate(tempFile.getAbsolutePath()); + assertEquals("MyWebsite", template.getColumnOrderMap().get(0).getHeaderLabel()); + assertEquals("http://www.knowingwhere.com", template.getColumnOrderMap().get(0).getDataValue()); + } + + @Test + public void should_throw_an_exception_when_an_operation_cannot_be_located() throws IOException { + ExportTemplateBuilder builder = new ExportTemplateBuilder(); + File tempFile = temporaryFolder.newFile("export-task-test-default-encoding"); + List data = Arrays.asList( + "version 1.0", + " Copyright : Crown Copyright (C)", + ""); + Files.write(tempFile.toPath(), data, StandardOpenOption.WRITE); + + ExportTemplateParseException ex = assertThrows(ExportTemplateParseException.class, () -> builder.buildExportTemplate(tempFile.getAbsolutePath())); + assertEquals("Invalid syntax in data modifier expression 'Crown Copyright (C)', expecting '$' after '('", ex.getMessage()); + } + + @Test + public void should_throw_an_exception_when_a_constant_is_not_enclosed_in_double_quotes() throws IOException { + ExportTemplateBuilder builder = new ExportTemplateBuilder(); + File tempFile = temporaryFolder.newFile("export-task-test-default-encoding"); + List data = Arrays.asList( + "version 1.0", + " Copyright : Crown Copyright", + ""); + Files.write(tempFile.toPath(), data, StandardOpenOption.WRITE); + + ExportTemplateParseException ex = assertThrows(ExportTemplateParseException.class, () -> builder.buildExportTemplate(tempFile.getAbsolutePath())); + assertEquals("Invalid syntax in data modifier expression 'Crown Copyright', expecting exactly one occurrence of '('", ex.getMessage()); + } + + @Test + public void should_throw_an_exception_when_a_column_name_for_modofication_is_invalid() throws IOException { + ExportTemplateBuilder builder = new ExportTemplateBuilder(); + File tempFile = temporaryFolder.newFile("export-task-test-default-encoding"); + List data = Arrays.asList( + "version 1.0", + " Copyright : LCASE($CROWN)", + ""); + Files.write(tempFile.toPath(), data, StandardOpenOption.WRITE); + + ExportTemplateParseException ex = assertThrows(ExportTemplateParseException.class, () -> builder.buildExportTemplate(tempFile.getAbsolutePath())); + assertEquals("Invalid column name. 'CROWN' does not exist in profile results", ex.getMessage()); + } + + @Test + public void should_throw_an_exception_when_a_column_name_is_not_prefixed_with_dollar_sign() throws IOException { + ExportTemplateBuilder builder = new ExportTemplateBuilder(); + File tempFile = temporaryFolder.newFile("export-task-test-default-encoding"); + List data = Arrays.asList( + "version 1.0", + " Copyright : LCASE(PUID)", + ""); + Files.write(tempFile.toPath(), data, StandardOpenOption.WRITE); + + ExportTemplateParseException ex = assertThrows(ExportTemplateParseException.class, () -> builder.buildExportTemplate(tempFile.getAbsolutePath())); + assertEquals("Invalid syntax in data modifier expression 'LCASE(PUID)', expecting '$' after '('", ex.getMessage()); + } +} \ No newline at end of file diff --git a/droid-export/src/test/java/uk/gov/nationalarchives/droid/export/template/ProfileResourceNodeColumnDefTest.java b/droid-export/src/test/java/uk/gov/nationalarchives/droid/export/template/ProfileResourceNodeColumnDefTest.java new file mode 100644 index 000000000..a8b8a9def --- /dev/null +++ b/droid-export/src/test/java/uk/gov/nationalarchives/droid/export/template/ProfileResourceNodeColumnDefTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2016, The National Archives + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the The National Archives nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package uk.gov.nationalarchives.droid.export.template; + +import org.junit.Test; +import uk.gov.nationalarchives.droid.export.interfaces.ExportTemplateColumnDef; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +public class ProfileResourceNodeColumnDefTest { + @Test + public void should_operate_on_null_value_and_return_empty_string() { + ProfileResourceNodeColumnDef def = new ProfileResourceNodeColumnDef("FORMAT_NAME", "Format"); + assertEquals("", def.getOperatedValue(null)); + } + + @Test + public void should_return_input_as_it_is_when_asked_for_operated_value() { + ProfileResourceNodeColumnDef def = new ProfileResourceNodeColumnDef("FORMAT_NAME", "Format"); + assertEquals("FoRMat", def.getOperatedValue("FoRMat")); + } + + @Test + public void should_return_header_label_as_set_when_defining_the_column_definition() { + ProfileResourceNodeColumnDef def = new ProfileResourceNodeColumnDef("FORMAT_NAME", "Format"); + assertEquals("Format", def.getHeaderLabel()); + } + @Test + public void should_return_original_column_name_as_set_when_defining_the_column_definition() { + ProfileResourceNodeColumnDef def = new ProfileResourceNodeColumnDef("FORMAT_NAME", "Format"); + assertEquals("FORMAT_NAME", def.getOriginalColumnName()); + } + + @Test + public void should_throw_exception_when_asking_for_data_from_the_definition() { + ProfileResourceNodeColumnDef def = new ProfileResourceNodeColumnDef("FORMAT_NAME", "Format"); + RuntimeException ex = assertThrows(RuntimeException.class, () -> def.getDataValue()); + assertEquals("Profile resource node column uses data from the profile results", ex.getMessage()); + } + + @Test + public void should_return_column_type_as_data_modifier_column_def() { + ProfileResourceNodeColumnDef def = new ProfileResourceNodeColumnDef("FORMAT_NAME", "Format"); + assertEquals(ExportTemplateColumnDef.ColumnType.ProfileResourceNode, def.getColumnType()); + } +} \ No newline at end of file diff --git a/droid-help/src/main/resources/Images/Export dialog template.png b/droid-help/src/main/resources/Images/Export dialog template.png new file mode 100644 index 0000000000000000000000000000000000000000..cd9b243b950e9e8c5da84b7e22767181b34abfd2 GIT binary patch literal 59773 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∨kM5K`8CSdLFH!O%cwW`l_Q%-UQ0iC1Md6!UYva{1neTh{p z6jGnrfeXRQ1;17--NwYs$~20G#nMnq?|{IOLGR`rKFEavvdOt3*0H2wx{4~Mpd3bi zAn&i#HoI5mjt_AM1)==S3seMMyb_s<{R-OinFt&uWeygf`(yKahraoLTGTyeiZj8&g1V6Mjx}h}dVXl_*=_6i)r9mz zz|{)CO&o}qD9}K4n!Wgd$Ue9zZp%HuU>|GoZK@V|$22~|H(#^rUq9&PBo|p>N_8d! zaX?U&mWt}{x)G~4B?HW1=(Z?~a6-KGheA&eZs~TR2bS-nZp!udcLYF3K!8Rc*8}g6 zzWK}!q>HqQ+|0a;Q|cyZBG5UC?-*jh{iNyUNsoXaC^KBvM*cyhhGnj%xMleIMR*)A zB0sHDqG?D1Y#aV#{l4FGef({@0BLf>L8%Nnt#^Dy=DqiZO#g+Klko62E@y?A1s`Xn zbDDo2E-Pka#zB}aa9N>xJLJJnk^RUZ#M7odxy2`W1uR~vhZ&Xr3Oe{Nrz%o2@ zr{B(8r3$60M^mMLKG|cMY39~~=}x=b5YR!BO9U*vYj8Y!OigyR4NS&&&-rC4Ij(x{ zf3#b#E{H7;&{z-_MSvj=TuD@^Ky~44 z2P8d4Aunq=Q)zuAO?ewZdNMh3VCl>CL{$(sZ)vRmc7T3To{-?m@b)FbH`jN-m44BW zl`lhjnmJO=v|4G9oLZEr;xd#bA8>(ZUbKuzPzI>7bwgWRKQ>0CH=j%dd}*`1Bwh6I zkZjTp9i=FDh%LxvNRIFCpnRy*@21dzRpf%GYgpn``8)w7H)7qC^2UCl!w}8T#%89{ zpKRsev{_YokbF{VZ>l+BQPYo#el@9IqJ*(Nf~u?#{#OXSVnvGdstk8h{NT(nLA~o8 z0AX{Dz)S7!=61kW12Wq5h46V_&lO?&3ktP0z8s*B&U*JywXZZD6|##>rZ^gq&HDu# z?^=8>7$l#ArUlO_RTf56E(ZlVvVmw}-Yo6?CW9Fzr}4U^?FF$6GW`LM;+BW?L+tiW_T#zUX~})L`U0UMm)qwt5+t%dFw;#)T>})f_JLox^l!rKS;H0#C*kNaNvMil z0(M#_kQ|Qt_0!GE`h~y9y&4F|44gVc1R@qA-KxXI4D-`Mw2AN4LgMaJ{cX&F_6FG%RZ?2ksVOQL8d+k=GOk6EhH zTOuN1Ti#E0nde{A#1BGW5jr;#f5A>UpO%a7E5H5ITdKlAiSsK@(O?CFD^HF^LE?YN zkNLRvQPI`tvMz7(4eJ>=ib1$uz|0b>hO6e68neVwOO;ViUdFU|$HC2>K zA+BJxJg@_=if2z>Z`K{H4UF@?k-LCJ8&=j63&lhCI4ueLT&PWL}HGw3&2`6ZWCzRMA z<#A#70+w{)lmM+FHSI|yx8%&!(t~O=H3o`W-e((~reQ`|kB|1~Ezyx;QY(+%1@*N_ zCkf&RZitHSc@y_@>k5WH@|B()wWwI!q@gpirtmI2-Sx59ki2{f68Y!(o%LQbmR0H8 zK!Uuu$j7rgu`7a?qt-byh}v#G9$dEl=;{p_Vc-^xX zk%`ul#0CQ=abr8xoFNfH>i{s5RL{E&OT+fqxL5G=NEL20YsF|Wy&5)Q^S#BW`R7@h zici26kUHndx;qm|28$P+t-KnnZD_dERZ#1D+Bs4zeP6RWFn-5U%3i0G zHh~d6Aw<@}r7sWDKeU?qbLpj9?w7)Q(&>{Cu{L8xwREqG8Mr{l98MEQ z$zJ9_-mdQ+{~Ls-nr#DG?%OiOlC$H}Lt$gt;e{Kdi1dxkC}wItHa7A_-JNlNU~=wL zMqCoH*sx=GHp|l?*WJLsX5`rKik>pfGS@+uoy^r3tk2eP%V~8(P+>UMdlsRofDt<- zxAB>Tl;3%}L0gXPT*aK~Xr-@nLD)xNs=>itp+bCjNgrKvma}|ah0^|#OhG>Vm-l#R zS1G&W`s80RIh!S7x!Z87{k`++vK1it1Q8*fT@ky&ePi92J!E!$^>EZ^#+5J(I%}-L zplWkSatTUjA^!MckueSmThPVwQfJaB*9TX!(e%HM`crhS*;RSaLNd+P8JQBDi6w5x zmII^8Jyy`l*e(mMp@VBZIoXLtRd}~-e=4^uXY}X%pYzRTsoJSE=+u84s_mQ zHy=FmzYVk5E|?%vQpSW1nsP+N+6w*IzHCOnbtY~0S(-KXPMQ_Pvy`XITr}P@wveZO z<5y)CPnV=5{bST)^;6h;9S=nCELrcyr~vKxHgBQ&QreJ*cHfzE{F}5lo`?wF;(rqi zaFZQ3em3jfo=H=DL|*%MeABp0V;^zE5Widh(;_k|t1^49?(5?+5JPrKtf!+uAzYu8^z;oX{x3t2YVcHgq(mqPnkWRi1IDXs2>XB6_aYNTwc3yDBS}b zS#WJtJ;UpQp{{(ZPxH6z1|d%4LMqqntAXkKSk@Y(_#J*B#nMl6DKL?{Mh#up5<$c> zyyFqodPoV2GiNCOZ>3ZUckl9zcXacmlF2!$s-gbQr;J*;X}*v9DN+=#r0eC?TYI|FEmPHa?M}aV(dtPSZ=WlgzW%q|_-tPDqsB^I#8z- zPtTL2WrRA0c17=?2!bh$+@V2sPQm)s$7qaeyzN@^NohTHDE=4K8JMGn?4Ur(0O&wb zJW_CDmsK3N8Gpy)^CDA`KZ_jrMUhqYCwBSz8o%CN^Bn;%?v!x257R{%%93~7%ekq zsbUjkr<_cf%M$i2Gh{gC8Bi-D18DPPL8h1AsQ$K}vxbcwqvEg!?5Nb^LlmDydrt^} z$qmFp+x{yS>Rq}#eqw*wmQ?oCw)(va>de{_4)FR?RLbdzhVf2~g<--PfE0V@`t7T! zKCMwWUfSV}rlKD*aG2!{Y<8QTy3#bR(~CbF5kQYKdUO2}*KsAPx&JEuS1>g}`~x7x zb99T!M)pNV-`5y}-y)P;p#vRzx|b6`7m#e1+p~d+HJLp48v|a0Z}H1rn01pn{k45e zkcWQ$9d?o7NAEUkLWD=7*NRSYAr|sfFBQaIh+MZ}_#B_zN3;&$w|$ZRIXF^$eFUJr z-D7V=UBlBet8x&i2 z*M;nZbdr~B%Y6&Bi4@9oRnbvCVQ{=L6My`oX!N+R5{KzXXbxks`@@1jGB3H&3k0u! zo}Es3L(=TCr^ugIS>1#AIW8lO4P@}fVqB>^2Vbu{=dX1MFYR`rc2dMa4q1npEmCwQ zPPNLe%iO`{{=l)*)-Ogm2dT4wp!#tJUvhnS`W=jF}Rk8){Xs@{(2WzTswuqCD7I;nk?v)eM z%Z!0&!bNdIdD0xsuy^=H72EDf^ZOld+LCtDivB^h4$0(5VplU}eu3~!{khdzWJQN| zZIH?B&sb3p^7%CHbRIa8!a7r^9Ye1wDwEuwQWV7(jL)~)X8*M1qTyC_WqrM@YC0`E zj5Tv{ryey6HpNxyUGg@xE(*?IGzrGLxP}E_0a0~tZAo=k2$-=aDa~EYWl*;-C{G*i zsZ6@mg${RM*shnl+-J$MsH^^SSe2Q3G-jG6Ug@OB=^RA?Ynr+o{4&i|R<3mE^#+iC znmsp>0>x-va~@fXBkjhKq=eE;)6c`DXcTmP2lt3Eumg{Wa&Ss-kw4lOY+~{sf@D3D zX=#01Sv@IioEA|}B@V=Fnv7osl|M!7^_~)PQDUNEX|y_v5#WS@bPto#wbpgljfh>D z*v1CdIwKHUjGQSkXsI?&7Br@Vj(;RqZ1&{T;cisO3XnX}mE4`?%Y#sll*sM^ZsoUF zdgKICA`5<+X`;TwMQ9ay20TU#zN9}mAz0zjk>mAJ$CKowWB{S7T;{Qg)0P)14NDa% zYv%|+q__5Cp!3EYZvrobEgK#A^0#j%SoGG>*uiL*gk!VaDp2#iA(KtWtiNqd4*3&|c$i8792|og8WTb|r<9%0TWtF5eBB)_~^w*r1 zygze5s=5;ElI#vY?P`pKsSfbS368jAO8}Q?n~h~>a#kSi&|XjUb3qnNKplr}*AI%P ziF<4kc17p-coBnS(OY+Cx+8xAn!?YZ6w{fImz{45~}v$Qy5`0 zl^rX^e*(9cSH6@%jU`@jk9yOPKok_UNU~RdeO_`fOGiqZ$_f0I|9a%~%xR+N1>P|H z3YqUdLbV}X9%mO&F8!mW$;T?^fs^Da8&|4<+6Non?q-28@*jiAX_bWsoLoI5Kw48z z!nBSrddfC?f~xR@(p;cNQ|w#e)oC}gz6+)R7TfdrrI2IV9UkxX!64{qxt&vG#0agl&+|6#4|QrV zZ3jszgaCUe&8%!rmCu6HV-jO_$PW^ot7e4();W)>~6#Pv6tPu(9xyS`yyF3%F z7}!G!!!;z}ilH=@%p=MTELJP5*l@D)pw0m6HNk0N7Os> zKC4E|rt?9>AqK=fynO^(wk%1ll@8r5*pc>y)(IvAw1}1k`UO_pCt+H5ws|G=x*GEp zX`ZVG3DyQo(h>GR&qD#sV6Q~6A@acyQ(<%IQpTLAf3rI<&FoJ%Yn>Iu)bV`a8P^*T z$co5;!`&mbU09l%j!tOyY76|t#X9fv=n}j&Olm%>3VBhkdXP*L9rna%5a%+ddTaAd zhRYeihfl>Q(~zYTyt<@RtX7+1Yy>b*>CrSX*}`%A6M7RS=xLR+hB%d?j(g~# z3NUJ9&akNY6=~@3?jRFZdgSe1S&XDaUi;cMu%i<6ZNrw>gwfJIrKQ`HN%Y=53DHWZ z8zRE7LIOaDblIJdE}5Q3P3tPRGBh0fujHMV*BU}@_+?lB*lmRup}k3Zw-_l))8KXE z@OXby1!f*Ot0n>2O7ul_T!l3pL1#Dz`ZSei~~Mf*ePG zfP6E7^|NMHm}xuovWxYB2H4Ji=1>Q-sL9KSS(do(u_J%P%|45$WPe~64}oNmfvBBs zDmy*=81o{hhVXsLUVhAZF?xAU0){q!^5S}hAX)Yn=FRoMSeKifJ$r{{e^gzBZ*P&$I|-@Q z69L$2!GXb$F30?r6^ug?@Z7N;D1>OysxHC-}{){rCd?~o}0vUjBbeLS}t~} z-Lr81-)4ufQqIKB+U(GtMK3faX?uJ6cQ+37gqWFYC#~iP*FOOfSEFm$mQON=C-57c zZ4N&=9-OFn?l_)nwd8M3dqjEf8%5gHyRT&G*+i5^G$VSiM}zo4h3~2&ULtsTJWu5r zAL?d@ELAmqR3~6mRC}^`kHn*{vod~f3LH+3oyKQ+gx{pJ4~>tjZhY$ztY8ld>{e#3 z`EUPC*#R>hLV{h?yLhbpHt3LyxW7!EC7OOx8R-$epxjz5R^vT<%q)~Gs>1lM6B4nx zb@x7$v+`h^*5G}$uoba2J<=`g^|5>BoVsa(%$bV56GuP5ok=JasTq z&*TXcKh^ihz`%e_^&3}+%CL^~mLgYTt;I<^Ofz7`0VmxxMxIs?ubH*?y3!WROr-ON zfBvcg*1pwAnyk)^F;I#!+y`mxzndy&7RS#yiV#s}pi^$Z@DxLpmvwQrk7iVnPinU; zUY!1u?ULIIhu$}Bd(;v8ykgNZ(%YFZ=|^gs~ zaFynPs)?akg)TM^E?l$zM(?MwV0j{46^X8{&6K^w#_CQiTvY<7obbgI&GDdux{Yp8 zdqKa)wFBBBNcn5EYxHK*OCzK&1`kaY0?j$Zfa3l z0z!TSiHJPR@Gx(sbG={Pl<4)JxJNqDI7c3L2s|pBa0Hi3elUfS%efX9o!r0qPHFGi za0Q<6F=?bbRkkQ5_@s|BB#HtHL6sIX$#Z)M!gs&()Vl*weMpv^Cc$eEiJeaD5+L#! zEUG;IDJShoHidsj=0AO~+hje#vz2|l-M}J`eIenOrjUX+{YgKD>m*NmTZ9F6DW)lA z@4ekyUyv>ZbETc!cdXLPG-u!ThaEI=728zKb$$Q*Dm|1f824l<(nK!#O>%Y;(2TF2 zo3W#RJ;l6CXqu)%l8A?-h-<&cYAPyj=^P|bB$S!Ngdx~rue{26Tc8vKM4g7)V}nw88tWw8Ve=Bw6Q{A!@FzrA%Rf+b{-%r zUvqo=Q6+le;m)Tx4%9cRGl1#nN~9JMz#ll=?IrrQMqe?e0% zb%Ho-i*iOiXxTcnSyjRnW4D-1p?&620XzI}jamd4hBQ1Z8N*x8egf0rv&I-ya*VT> z5r`IZ0gKA7uoeROKrzF<+7mmlSE$-rx9Oa5+?tZ8ZK}XiLkYM@g+b6l7MYvSDeq^o z%DP-V-jRlt^gC}i*4bIynY7G#(|NZx!w;GSv`sUP7x)|5$vBTQp7%$JRmx7L^8@dZ zZ8?v}-m@hmn-vjYMaiP9m&ScCWKY^LrO)gb=C$V-$X7-2S32MqS0MSBfsBeEjQ>1{ z(|C5^IvZ9G@;)v+q-%RK8izF6tM0l{*SX%j%+qZT(Xh(+L4!sZVP0CAgPsH-o*dEE z%CtM|*`lx>0{?it{5ZwHN_;5uHPZV?)WV&axfl?Pq!lG5{8H2V)yo~``rvqlvk@)cml9I1zqnl~$qAAi=@F+G7iG3M zKcIZ{xS-)v{iC_o77n8=ypr{Hdt8On3Jx@VP0gV{BBt83NZ<;xzCDOkdI)x-+9(bV z3xRT-v1s&QnGN$5Dzba5o{0~i=ee*u}mK3|UcELwPa`SIa90IE^DLWIAu!*3N5@!AL*=Bg@M5te+y^*rC^YW-k&eCONK{R`%Yye7nzx5B(Q$m2b0Cy>XvA4f4q22KK#~0>UVZ{F7>4;RE3sa=Kwmz}o%-(+kx{UZJFq%mat&pL|(N9$<&nEMVZ7cu8`JmLC4}w<8U4 zkwQZY0P2_uTuGkbhDhNj?r&@%>@ZPKdY0{M6(Qq>M7ne6ik2oIN<7{NNONEY*eJ)z zgM%CWp916K!<%Jwm)B3HXAbBG_}7!w4~FwfvURU6;dH?zHE0Oet;eNi2zCf@S)b*j z`KB}mbO}VZnU2VG6!#sIPcRGj{hh?6IZWAf6gfK{#s_31^-SZUN2lJ&59L~GH@LvN zN70*+pR8krbQGs9$eu%&&!q*PKARw+(Gz-}%vGW0GU<`>O^uG|0=-cb;B@%*M3$Z} zqL9MrbAK(Zoa027GN}gmhpwLEssEi%CKRU&0|jpWf#OIyyZ30H`12pJMZMbjp*&*jrh?nrQ=UFeZLy zuBPF_GotCUF{DmnJvQZ9eu&H0!)6ixS>Y|;siV4HTrB0E2^^aVn>jiX*s z{=iO_>8Mrp+f00Zj-K_rp8fI1Fpx=51SkfwpJhAp9&cFvV-`o!VCJ=(XtSw1yjyQ% z?qEcx>Hd`;ibvCMPH)1`UUS8Z(|OvI$GXbugVgs?hqxRgA&de!3lw5_?lOxfwjq78 zJVy@WcXc?60_KxQM6ZHFNxVcQTL_Bj+zX}PN9Kp1UmN^#^-^0o4d+@1>C}XwVR47~ z44uNY&dvrcEJPGc6#VV4T0Ofw+}wU6qAovtN=+C$!5He-iZ93!>7C8$Tfd;p*VbyW zJ9)IT{QlT}%IEtrBKl2NZ;R;@r}6Rc&qk)i^{F7K{xRD)IH;b_aw5{ePhCvQ5<0Uc z5>Cu*e7uaz7%?;Fnpu1Zt~yxXXvy^va_g%Rw}SD^=LS; zOb}3`;501}%tK9WQ72Aw88VdhmxO+!`OG z)XiZQ&BZGCE;=|JZ`&fF6P^#G6`WjRGMuzekOq#|y&2Is(~z3sGPQpjr1?1hx9tIM zp=w&ntoD4qe?Zl&h}Dpkw#erx>kV51GA-djggKM2#nkknq7kl05N|VMhFum3Vs^WS z(Cv+|V5v~twMyZ5IYio0eC+oY&#on{O_xkf1BOV0<73^ns8jL@GbX|!Yn*!sk?O0~ zJt^H5udv3sU-GjDYde?`Pu!b5RXl|qd4aST>9uEU=ty_nE~x~A>Loe?Eqn*7{%b#Z z@#qqlRCmCC-28)aP(jv`_PL%DJ8nu^IR-c_1CDo<30^UURH2Ms)1ZZg5K=?XCgDloUQ`~2$;W=5xa zz27!|!F(71$JU^aSz=E!`-fg{Nk_A$y>b%ktwt_kYVk&)=G58B@prvcQ1xc*dZfV+ zzm@!1fnJT9V}aO-NjUc3o+R3ME#!GRL}Guh8B00OYK{w@LCUlSGg% zlD5@nWRe)MB3a>xKi>ZEYxMcG0lQ*8!EKYGV$Q{~J1QQx()+c7G;wrv6Pe&`4eC&tuT_ zc=DzVI*aRyrY;E`Qt5s$TotNw)~K;qpB?rsrJ`Kh@BNKS^-Wk1_jVezRX1NrG{LG) z1Y0G|)%ZY3e3_ERGd%u@%9(WFC{_hKSzU?CC4FM*@^J-skPNcyU8$#l_6Kps*=CN{pa)Vo=;yk4^=m5$7xy4 ztS1&klC6D}=mVHsnO`B;VOIiXpGYR1=?<&>GIHN|x$a?+chb;8t-IxMom47ke4qZN zdx#=GM3Pz?*<0ne1HoNULk-nL!)wQ?`ZXorDre@i^0^2`zuT1) zWCg)_u!{XfqJuDatQX5orgJ_=QkKa>0X!#}k8e zv(e64p>@VIr-UL@1mbLuqOB{!{*L+B{lTwk(+)_QYfvuJ?6yiGbX^eTw4bleiA-=`8q@jlfov2&+&Gq z&5{Kj$*CYcWRs7>mRpACn4#Zeso2NqX?RHMN}e+{T?NHsM8z4J^&p~Zw$p(oLaMlG-Uon>kail?p}{Y2fj zbIRD1U{@_}KMc8r*z_Yg)#>F%%s2MOP9`0pc-0@XznMp^b}NCgTJ9-B9m~wqp61l5 zt8peW1mhqJqeO=mHgG-Hlg;7OIW*G2C7EXfrIAnJcV>TZwR?E^v)AhkXPLBH2wgtL zoD;MAk>SaI&_749s@rg)PW6Bb(%uQ-Fb%cRTIBAd^-UXhWu1E&hwmb+ty=HQ2NB|V z+Q#7(beV2)Y(f>Tc}E~LSXO>M0O)8IQJ+nINgeNrCX=vkT*Kyh==pjZ94R8JIxJt^ z5#$zfw7UT2y31!_xxQ-ah>+MewurVfY`-2AU=@>CXnyaN7Gv?X$M1cF;71wUrgGDc z$5JHwmx^4Z{QNx`E8aB=@;-K6o$+gqmqc0-k(^ndZ9gGTA+#rldH79`iD&)lGX6_6 zG{QxHf@=f@fubKOQ&aWXcF+k@+jSZq?msqUcU;k~MIRmPopntpEh!XG#d0tih(+b_ z6~lii=2z*wIjX~ZpjsU~!tz0Gmc(GHsz+yaa+S#K%7q%1$UgZj^y?UiO;vyHp!H>i zJKcRhDDRSu4xc{DV_$8(p z{3rK~o0WqVU&E@I8Td6vh&SmTH5+JPMd*fBho@n4)2uYHrqNAyMb$%$GYn>S!>Y}y z4Rf>B2gWKZ#Tgmnet}%p_gym+@&hcPC05K_xk>0O!&3t7;F3hdwpF-L_ysO{$xo01Vi9-w+>9D7+(AAs5EbT`E8n`g1m#S zKae|#FN0+a2he%|e#h25f<~EoZZ@xrZ&tiMfPwq+T_j+IBm7S_!=A+z1cyg6_3k0Y zXPZUwazC@hwM87+m>%4_?mNx`t6wi=da5|%<7p3{t7T*K70MG;#Wv6)^P7j z)ANQ}qZr}G!#WP0G2Jb-KR_^7@j2KLL47)*Fl6an;kC5#3h5o`Jb$BmNb1i8ZLUW5 z8oraMpRryJ^06bgRzfF{@7dfOO85_a1?{$%g$v*UuG%3UY?oAW#n(6YYm)Cs1RWe! zz8MY$>i()y_#Hm9qvYJ$K5?FS89~TzG9UgWFnmAZPA-*ZNWOb^z*kBQV=dltvjOvR;T5NC6 zH7j!D;LaI4RRkTlI+4N70*nPIyZ3H`%lnCswW7^~JOh_3x`K|ImEw1om8kG38-qXA zKhW1y3#TLX=INyh?8gNr8Sht<7SklLL2|RD zP1o!9hBV3!gwA}QW4tk;HV}V7Z<#Jz5&sZ1@YD+X8Wt+~s$VenXN?A_R~E#nJ<`Do zto4Mgb4$R-A{o`VQ)6~@SEzU?i4@*wC(y!4sSpaOp08Zo@BNgHev?tn^JOyWt`$tj z_o<$fIjS!q)5pE)g?F(A>J=Ib-J}tUk1tcx=A`?q_5|_3r+jvf2+0o)wMMXXc|CBX zdUvg7M6Fo~Ky-xwmG9j2RO*tZiyS>ydY9sh1s^+gCD_>gH~Omd<~DV8+;%P@&Kzl4 z9sRM8X_tpOtJ&zTiS{MalKQ9MMVZ@#%Hq{TD31f?gh^HuP{XI)bQX^8& zYn$pmsay?9;}EfQ3Z)gRovZ8WHMLZ7|BTJ@pgPJJ6`53^@c1>uc*!m#;XU810vI2N zo}mKKvnIt!Vt&hq3EYUIz(?Um@erWAbgvQtvFc z8JqFzUroL<=JalgX?+iv;r4vlE0`C34+S;b|IF|#9a*-xLC=jpi(0r^kSB<91!>i8 ztBk-Z79ff%ZGK$jL@a;5gpjEVkZQNY#0HRlDkxM(>+v<8;{Oh_7sR+Ptl)-5ESV7?-4?V14nYA4IX2f2rvd!tS{)d!m zjYjSxX0P;+K6Cv$Vj0x#@ZGW6>;pwEaQc{#gd8!)D$-@^Ov$EpRa}!&=Tym@Tm(^- zwb^fI)4Hk{1>r??0nw0Ydw=%%)w&HQecCiwf*|zg8g|*00%NRPP_QFbav8E+ z%IBhGqK8?3(Xtvui*%+5BDSf7GG|k`CdUPVRB&>@-7N4)I85Yt#P@QI_;Ik46(N3oO#AO^&-VA z3moah)$bx7eKI~Xdae)Bn1H78b(*(=tPe4QA!7T9KquJsLjGfjgpY3avYFhbP&Er4z@F&(FBSX4cv@Lv8B=mX zLgig+)FPb!U&;6wBog~V?-!oIlx;3q-+9QPegB!^4$^-3&x6i^$3Ku zJX>6{YsbC)7lmT|jfYH_9Im%Rd6c88V|kIZl$;x;?cnAf^LTD2E!7_LrIrs$qxjxI zyd3|{C7Sodnz^5UlzdCx*{ui6w)k+gi7s=BPUHI_x^=i034<^fo3w4V(UexhIJ$lv z5dY^QP-h08D~{U#(3VD%NOZj1x9N}B2Gj#Gj5>03RN`Izf=wye24D_u8eKAml6|wg z0gnwYY;K8ur_G;8ANtPyPe3F0i({#DPA8*fKspEEmKC4Sz9Z&nBq0Hgkty6^!+t~q zz!i)jtyLDD6aZA{8<_f5qZSC}Fc8OlFkhn}CH1?nNB34j1++02HcvEF;*Sdqq*9yf zu>(9MB_+0tjnz)nv3WXzV?Z{D1>Et*gAsXUVtCfuVYHt6=Cp-MM{$=}*vY^t=q4?P z5d@uQ^*v1CYjjx8Ru)vL{5f9lZalIA?WyMFS4flH36376-WX04@*Ebd1Cm1!hjpu+ z#LJ5L#>OWPN8lcZzIIpgGpRSu$p=Zza3Ck7uEDms(Mbo`2_o3I4L;o*f<_h7Nx+5@ zd0JQjQ_5=>cwLvBv5!WT7T9c0qU%~nEFG5TS~D*V5fFNxWxXFU#WUktZlt>-q7+hL z197aHKeKG9n1Ut*WH|s^0!#)4+fo;v2`a>a zLpy7tZf*Z61&m-D#vI!-NwF*{`n+GDL8=DFDGspo?Nu3WS`82z2No$y(L8FO(J{OM z2bE-gM4Z4P0&nDm6QEB9mX`IIOrEO)BOplX8yOiz{ril+E>2E9yk0PW&i-gM`0Mij zZ9Y&T+1ty{&zETTT8$+>B-8xAnm#`UbVv5Qf%hfRBUPLU!M_N62#s9a^x{42+q=)` z#pO?hDbNo;{!@Cmqnz06ziwAE?(^M$T8nsK6^$I&`J0i35zas#FF+y$*6h&sng}$) zjXsnBD+t6LxTSwR{?!s1wXdE*o8SDNaW}x(0ps<5qgA6{mrs~+WzUFIpxkjeH6!4T z``_;>|DSBTGBhAU1Ki@W)W4{h|97*+=dJp{Tn+fV>t?@vZV=%2{a?0B$EVPVmXH6} z{@{P;9{z{b&_pg<`xf0)Xa1(9Wlg358XB4dVY_VC8u@34^Q2$k2TzRGu7@-I`k&@P zbkqMWgUS%~Pf1O6PpgT43BcfF01lpBOEW;QMMg&EyqS?%`lXbo(c-S|H)27d-20W?h)s>Z%hwz4$qi{hUP;@8>zEVUwlT4-iMe9iwkEc1+jb^)CblQGz1Q>ZAF%hgK6-Vpz8h6tU3b-W zil!tbq5aZ$as+zZZv)m07oII`zdFB|fRyCqZj-egQ(4Fi225|7{?V{6>66FM#pdR= zADZaXn#n-Pa*!{Db-r$=U> z#2VX#1nRR1p(NG`^zukYkN>j_kdLb9fROk9ugCDe_rlAyq1MyobvR6&tjkc0!3o}T zDw1(voAAznyO*`XCQwUiw@-#KEsxI=_1WKV)+6`%?(XjS**|Nv-x(hZ6Z7fi)|khD zDd>63gcTAJvZJ%Jw@Z)+=%oX`X>sd;{GPHUtILBMz4U3b6dARAp+Mlt?(OZ-(9pnz z4C+B#!~bD+nSFnK?PzcB4hslTmjUib{ss$ro#DH}8{`{tL)dek6s({%yZxUW7SI8p z-5sa0GU0&cgl89 z4H9N=SFgEQlU9v7ZzU3@FWRN}4Pi-X>0kGooace{yV9IZU0q$-Wad)ZC*DjmF%$mP;>UWH&k@+`Gn{c&6sT(d+oKIaJk+Xwz;~~OmW?bFgDCDs>t1k~I5P5kP@_N!lZLC;G#|9QOU_d7Y^P-XtY9Q9>p zW(^72JN15<_g(Iu4R{PKgGrGw6%rC!14@`RGj%P7sPND*M7UpX_If{GU23~0Mo83n z^jNEFWyE9OQqK<+hc$BwEi9eh{a|~8`4!{d#VXg-LQ{M8?0!nW<>Cwwaj|K%I7fM| zv*_$(i!*ep^ZT4GKAlKx?j}!i{xTIN^S{>xHs`p~|Iv)@Was^)6NJQ>uU;|#NzRh3 zq%ytLMvj+U;h9f;|K;wuon0(v(khp|>uaKC@eH_{ zzOT2cy0>TR)w`&*W5QE(+Kir&z9e4WEgZ%2n|yvR^Q8?j(u_z4xKrkS8t%g6zF9#_ zi2{_FkL@m_KY4|6chv+D*n{I3VxLf1<5DrEP8e0G&eab!=pbKAEI-n){T>&`ldu@| zf%?W18OEbbTTzGle|LgY#*=0)R$YHiWT(VQ1A+^p{&AguDqMmu@2`j+n`-hr%i>vo z`Lk4~r{0{rmB?z;>UB9>o#g65&*-!}Jx(0at=Ny2z}nAG5EtYjJUvevo-hUca3Ayy z^E3hqu9qp~wZ@r@(B?dn=(pLmb3Z3{amGRs@q6tr#?-F=wB3m5!8TPIUjpjOSy`jO zPgb1N%b5AbY0gB%YIc;Lm@OzColdiG_c$YmERV0h4UeHe;nvDCQT_~7Tc?s~#=$7= zGq*%|=wm`6l&SG0jL`wWSIx~C)(m!@V|TC-II54UHYYYI9~jk~oE0}S(Gs->V$4-b%r0nfY}3qy^7r+wfqv zrU3y$L>7wnmr$=A^9n_=LVfydsZyg(sYpba$y@^YbY7TJgE~bfWYBHO^rVfuB=Spv zwkU9vr;i{;*{MQ@)IYQDub|Rt9}|E2U+^|L0Ho2*L&vR`#cU1sm~Q)Ap}*ZL4pK(N>M-EN~b22n7}2&V(U)#tQ%SC8vI zd!4zJEJjgQ;r%O1jfBPHavUasf0a_d{Fugc+o5p*a)Afql=NDF1vtHx1*LMpCsMD zW4=cd!H&?qj~ovxy5eFhBYtjWT^-ZXZk1ykH%)zX%JvC$a8~T7w)tLAS$EvO)c`&E z)9(HJ(p6s;gP(4OAElNBeTF@IZT#pgi@*DmGMAm3CV8e- zW8D*KszXRem&i|lv2SBj&Gt^JifPVM*Jo%w^DkKX6)!zIf|U-@FIuJgg~1yA!z8%# z>AuUeN^YyEAU0esl;Ey^{7v10f4FsU1y(Fu2aoDZ5yLu;zPRIYg{5#NYq%;+| zb-Q8jtt{t;Jc0%rxfWyj{;k<(IhhI+yd{dA-c!rHTGt1&(q|RCosG%ju-|Sse13Hz zhGb3@mOZGMyqT1Hlf}lu3%i=e0Lf!k3JOGzV5QTqMJkQG5Eh2`qrw7(;-01%;7pm zCbng9UJE+x&gzaviHk2<;IHwv+CoXJcKLtD+CtxJc1u)&K>l8nmA~BSbv*d@r>x1d z)Q+wa#DwvxI$W^Zc%k$cp8G|Y>IH}hbb3p_=`xULnmu!LR{aEe>&Oi&iKtHvZUsw~ zgF}TxsPJ>4SvA?`P19-n2wYoepW`x?)A*x9_gv`Bee^gW$w7o(I!e$@fcI$3ZMWlm z(Nuj&?CW@bOhtF6;QPTba3*)JrrS&5I$jWI3x27a!sKo|a|MJL6}F zA)c-Ex+atDyU&ZMWmg}r_4hQ{bZ~f#L?F}4<2^P=6-p)~-0^1ReoIOE zfu#ksr#Blu*{ks`KvdOVPcoS|pFVq~(3@v_(kP3ueQ`GK*%*UK{kt8Af{t0&h^Sly zxPF9LBzfwaPJqpF->F+8%ak__qH0voaxf@Aet)jOX(iX-jkGdx@*XxOsl3ISmVLo8 z`kwwd3F+~!={qsmJn)H}4=GHnhNB9iV+v}7OtDnMzd@g5D*e7&s+%nxb6U#0SW?-T zmHAd0zZHy&x%XlsYX;5TBieLJN&fg2~-ci z`=-+-K_2S?1s3gVfE+OY`)87(&6P+j1P+_4nQx#Rdo?zY6_?J{qa8dL?9H@lDysTD zBg{p(X8LkC9-N7{nE&rVj8wLLz3P590*#PR&~m>W#CmglL5too7&OjGXh|Qr?K7Ij zW;Zb&Vfa{+8+6$V zX(2aNOR^=q&qcAo^c)*B;y-IZTo!YIHaH{AUQh(IuEjuNnv%$GL|on$g<_w*i&xq} z3ED(N{liYAJa)tV;#jXPaVLbj4>BRxO()CmD_N=Q`6SBNQzAttA8!#H;UtUK5dCK= z$x*osE=Ly^X3L*h;>15=aI&7LLfs*SN8KXX>&?hClF+azfd)quB}(QU>+cbrM@Az+ za~;`XjGnYQ@nTH1x;tY`l*@clV3s^gndo7-`2?YI(%+Co8FOXH+jO9rS>xsbo8nov zH4{1ELK1m{n3as>j99mc?wV+9y%De`$^MhU8^()*d)eT1-}f87V{&$kc^> zZNu&Kwi;MqBX6S)84-yy-XQLAfMQG)8+nQ*2WZ~_c)M|yC%O9*5$Dm-((*ws*{s$Z zt~Ht-jK=xDU$*hHQ&%HceyEJQq#`ylB4qhuqBA|^$%D(SgGX&vMfAeT@}NWieFDI{x*H&Tv>#ss_o}{5K+d`d z6_^&OKE3cu#_sZZKL)Hv>J-GBD2b&er$ut2!9gvnT}JJBwfUd+Aed%s5<>+!i`*x7 zFPyCz;rR1>+u5#NLLc>*N&bF}jz^&?@y$(Ojd4b49~B5q<<5*zXNG00C%d9*FtJCZbx2_NIZ-ia%xOA0qA0ET^UG65F$!eQcFA6(Ct zTNd+>38`<$y{gkzn}Yk}i{r7w9@9=->O9it8-Yc)9IB6l{u23w2#Zk;W4FVUa-cYU z)hSX};u)|J6aacG2D6(*N*oh&vAw*kFZ`XV{`%8?1-Wg*G`Vys zXaR9BFsKsl8vBwCfTB}peu1HWM2~ZTj|e@BYUOGnM((?zzty|2ncDyMxS~HAgQuaW zxXUdO4}^X&>UO$}#gh>Ey%da|(+cfK7Coupu|udGTZ%d6GI6G)!YIcySd+jU>U?N5 zJ4Z49KB7=U4&GoNjG9dWB~T{-kH6U*K5W7m%{$rXaW0$Tr>qPAsm(wsS)Zz43|u4u7aIx7IDK$z*q}E|2R4P__1%J~7K)0%c%Ovb)u_$8Plf zA|MS50RdWpUaDniU5-N?Dqb#r*`x|p4(l&HN{x?8IdOQn#7ZGbo_B+4B8!$m4M&&5 zQWo#76XfeL^22UpdhIS@C(@feV`8p;G&PcfXBcI|m}?09T#;G|RusI0KEbt2$ifvm z@=&%*MWdm$Ab4f*uM)d;F~)AyA-W!zSEc=|D65nS#-G85)8|2FXNU*?7{{Y&FGDDZ++AXh^7!!-ZM1K;- z7O8S(Z4-{4U;V%+9}&OL1XFCzzjr?AN2YWc1C!rQL$ecAkvbjO!S}T&(o948FYFT* zTKG^QS~2nKdiApE`@)n7#Z=}PJ=yda$~l;>*$$F?928wR>C|LFMx~=%+KOd^M(W61 zkjAQ(5uZ$n^2z-;d_I?#n~KE=I!s#rxq#@FjbC7vH>4@!BGUb?dOwVsGaU+H>g87pItD5T}4-Xu{n5Ib!aX&7gKY zmwtj0$}UFDC~8Va`@h7pF#pc+2p1nOiQMfzj4xY#(;8=OFVK{`>YybmY_tX67uWZq z9?w(xIF9Cpe-UI`d)+aCv~qZaa3wTc+io>@Zms9FnH?Va^GmhXvknt$Y4=IpJWXyj z9ympu5aL49IBVf0yGLLFIAT$&Y=IJ@>snSJV7! za$7C0$S{X~;C#=CW6sr5@9b9V#f}HWt<{NSQWyj5Rtqh%$FVs)kGMR|{_T3|>TlH6 zJLjrgj6zF>lhO?Hzg;ipY8}P?2!Xq@iNdmqv&iuO1AVYJ1uGNUx=z+nLy5er7A?d9 zRCwvOpVO@zrJOxxx_lszN6Z27*+wtWByu>m|s z`1F;9G!)lXciNWJThHg~D^n>Y6Y)zap0wbfRm+fKE!XQ6#-mp~Mo`bO9E!CZ6T#7} z>=o>UxeNW@rjezlRFfTrV1rSn;@OnKWQxA2vV9@`Qs0z zUe}Ga)XT1S65L?W3U**KYzeY3-d0~m6Ln3rJ@Q(yqZo``UGt;{`5(1G6N!@cS^P=z zuN^FD^1ypb?#`H%0dUwa!0|+sVe|sg4d&@Ha`Gl9}?#uZ2^-xTrrP2{DO$le?H0{~E_ zL}M2pUzW*|s`5qPaW(`ETVRh+O-_a}3COGUdXk4S3KTQZ1M3k}ZO*9KuUTfuU!l{a zyC|2ad9qxq4N-4i_e>jmp#phZT?YxS?9LbJ$H|P3go}iAjUR|wwpJ}IL?sTO)@jwe zkZDCtmal3_O}^t2Mk}8-!}`7cvnfAL!4q;$zDiWAg{LQ!MEG8LL#M+=$`hKkdfcJ` zjXY!`d+RLO#V_!OvT|{H%E;@-AY9CV#xGBs@@QU^e1v1g#Q1DC-qEh5ts3G8w=D5e zc3fM7b2kLjt{%c?@f!C{PETzxcp;@lAdAe`#|KEEw8&#L<-{h_1qSP4YK5(ypFLY2u|bvydTH%e`70{GdoThFQ=@l1>#2g zVE>rS^|@#mzu#e(^Kx_>Rh%(oYoc6fG0mkY8kIX|B*z|)Mv*-V5K2qHg7n|8K&26!lOr87v9SZf|S18!9n789cLeZ`HEF}fS$Y|bp z3+{R|90hV+>U_e#=+z|0hzRqU?5=ERu_SuWpH>8#j#m#A?NJnNy%O6}jn(ir7Q66C z`}?S>brexO z*S=-G@7U%E4tr-0ZWgBIbCc9_xN7PX3T_=t>NKXSdh}q2u$VBw-=BRd!5gEoHXA%-kJ-oNy$vpVV&0&Cx|O!EEgs)9xMBZ7%AI`hyVh$QeCEb zTP5It>Rp0a9&d_JJH(yh)Ufn0KgeVX#~?WEr7-n*aQy!b9%a+=!v83mS_w$p_tcQ> z;Hod!U?hq!~^&H-eq=F%l2`5iB$&HEOccv7$vdLs9wj?9){9d2?tlr!&bLwlI)+NdK_{U&K}@YQJq1DnMt_TF}bQ7C2?BEkJ?H-moe%DCn&#k$RM`~-$r9-?j#eB?aG{4bYe z7RPI?x>$Z-i#rohG#cQ+;xuZR8B%KTqoPeche?QLjz1Iw>p8RNId)t6x|w}~b} z1`Eat=ALb^vVPyUrX6f){3vp(jyvI3IU80=>SEFW_K`e+9E|~Gy@8xgSNMF0vZfOe zA7O{CCs10PMM#KXHOjmw+3}Gcfcy1^W^l#Ppvs2AkpcFgbr#~~Mm93Jjw$jz(iNW? zJ&B$zF9-Ehjn#Tb5mTiUAt!c;*;HB^p}aa=fq&B1Wmbhn5V~B1W0~_rb}>o4aT}p< zbbliSmi#e9>p6C@oGR|@1yAZ|MNya=hCJ2$rB?ucy9=|tsuPGL%zH^NtjBrobT`ZI z=A50ha?6#b3c)n5m*yhrS&pxEIWA?SQCS9}NJN7qg&{r0rhv{yU|i0DBCreVX+Xya zb2t#touhD+pV(5k`3c2Y&Zp{S9~_!2W|})=M%%`fViu?B?RieU;)Ok~iLp#@p5Y!e zI#Q)Sbg$y}19RGmi)+Sn<5YGa?`ncnfY}=aPb`iy(q|CnITdSb^@=lotup zonV7kX#9pYeveeCyLvm^w~vL-f_3vtE6 z8n$fMCdq>6NZb#${W5$)Lvk{_oE&214FBl9AeWg;#LhVV9mk}Brr4qV6V?l zvK86i$(uK^)^56zul+F)V5{D3(uFAR4BKWKKBie72WNX`^ zMh)=L2_f2GXQ`tst^B4EA6{6L+WXt;vnC;FmKm*NX2cu(=wLDLKY4z?rDe~wB9v)LcQK#)DyEU=D1Q~kJruK6Nswp)sofu zTu1AsdeYpnMSSNtI+c)Qd{KGi36Q9g5#>CbLUsK~faqda#lfvK$KjI+6VyIRXvLsX zp_n7LBz6iqI|>lEtLsRR$TI6Mq0{hoULs(4AM2zICR>4LW&R}L> z@ANpNi{J`I*Vq09=|^1bR9>yx_YYlYTC0Uc&s&WG*EDtISvero- zr~ja~=hXW8h_XWv0yF;#L0!e!|7t_jaWJ`4P;!ZiR%+1ZlxknbXB{aVF`kYs?r@AC zGb9lr%b9~j){3uP7W`S{<;hY*SFTM+z$Acqz;Z=sXUz`_g1XxWk0scn0(pZFD?x>Ryq^I- z&6g_3>pCh?e-X63x19s;!Aj{P6e!u z{t$Dg^3R?or%e$R%lqu`md#YIsFwg2CaBy*$ChI`;uMP8^|3?;_U=X18#J!dZbc!$ zY8HUyu)G~Z7d@(W{BZ`-zgO^9_MWAUKM)xii7V*GH`z#7hbOx5Fe~^Cte^3{gi&X} zww>#E`asuen$}9HLPF*$QC|q(GV9B&4HhDfe>>!mtS4?8@ns8gb(Tk@Ci+ zN+zysn@pn+*X_vuCcJv45i!V}nv%T0GZGc^BxlkGevLYZ#j4Z_u0=dk;2a2W{ zf9}`^Pahx)wsS#G(vDDGON)2ZX=-YUii`~Sa{$>9b;C!5f#IrOHxML5j6BAFXwUFFWDzTod@Huo3A}ho-%kE9{1YHw&(Eo<;ICaVB6b9Vke+Pp3zoP;EaE9~$TFiJu(S#75W1nsZL3B8I z1-%PUTnR!KA3y;7@6S32ZVLWLx?att9^2nqbL3M%Avk+^d5MUK=;&+?M`6Lk!I4YD z{rDlKgwDmcNBm4D1ONrLiya&s1VRFd*s7|k5Ff!oK_*yN2LE@cU-~d$eaVU2?T$mX zu=JsZw6wJ9wK|@!ccycwRjRc4<+OBkKyY3YumYyMoSKM;=!InP9j67LtEt)U^Ln?r zspA0i1CCx(Pmd4}&--vJLA?fm^8?C4_?l5qUnf71d>zG9IUF2yv^^{$Z~ zHi&u;5G>)Od9m5kr%fCln6X@=4djvzDJzka%JQ8<%)>4j+CfDVv*09KKEqObqz}BV zbRYoF_m$JJ3oiy0!f|gf1V{$%ds??P9*wQp-~jJ8l#`nR^3k6z)|}rSEE6GqEiNI` zf?G}txZLdS{Ru-NMB<>s&=Lm)e@W*oaynX8s_HO2i;oCs0$-<_GHi7|@wnptAGeZo zP|#3No%~5jNh@{wNb3jA$1@$iz;ZdVI6g2wTaFF29o<1SsHcxJ$Rw|6&*L&8bx^%r z9T21Zk8O?i!OEo}QOxH4K9`L@cG5U1*EFiC> z;o}%<;Pdfm2ha^HOBuXHWAhShek&}ebW9vkY3*5WdXIil#^KEKKHk!Y@=E}9#QB3!d;tsQruWpR;U!;h)5g3peH(Lu;y@r+45#{lgNGZqt zJVyL}+5i%LIkP=_MK`Cy^aa%VE2vuv5P$xGqLx47^MiWh$uUuW6+Y+28Uu}1?$`oB zs1X|sxPQzIB%{clAwa0Db5606+SNkC3l};q0at7ufp1o0WYPgge4str0Vsf2FgDGO zBnC^kLqcb-x?f+3);&B*#&^cBS1L0W<#%)$3koq04-W&&1cBt;sRLK=PPfZ5j%N&9 zc=kGsFv&RoaREE$LD1nK?N-~o-JXLzoXMtNsG`=uUQdzp2_F!NIYOr3z|Q3$YRC^) zfV;$*kmG=5o3^kxjhhOq{&_2IrH*KkSg=v#=@w;e@P5Vf5Y&wn0R4|O>Zr#E0A^US zDj9JY?U@gSTE^H!LOyUdz>i^|V2Q@a#5Tfd2R)L;y?CLsXt7G?Uo}UCaG8;n4E$xL z6h-YAPK>PLUTA+m@ljo@`uVv@!_eAK7W?l8;}ufrT~i!kX$=zq)a|{xrz(BqiU2)s z8fiM>wAFl6y?c-u1pV2NAx`cOqCdH}qW6O76#Q_$JsyOpKJ zrJ8uD1)X|XkG_C+Cbac7vKZZz`d_~G_1GeT(0_Q{PZ3(76QtWPBhzFV11yCsc#4;f z8~Dl>IcMgZeT>q|H3F@^x*9Cn(vZe^<{Z+^ILOMr)fFX;K`GK$WzRo{G`e!ns&rTk zUO44@Qiou2^!n`N%jpnu{Ao;G*-KcG{Wd#<``TiC1nD5ewSBSku5m&7EqWf0X@cEJ zVplrzEmosF4o;Cu`F)$q<-sF^QuX={GNd$Y2znBx#)N1O*pKD$jTq6s%NQSZa z+R@a5H-WDhlszziegJc?di10HWuJeJ$*)MgI0~hLa_#s2CqES;a(0Fl`>>s9gWW*L z!!_#boJqPrhzLO(y4MwKse&E%vC;3P{hM-^hXIJq!@zi3&u!$vg)grvCb8JH-l@ge z*ak7OxL>6dR_a3!Zpf2U+_o2=-f42&E+dMAY!8w4R>TI3*_g53UsOHV$ zr2J1xMU!^E#h+2SA5eVG$Ke)=)Ji=0;}c2Q!bQU+x-4(yDLa1iO)RDGI`xjT`@Ns$ zJwF*`BJ_6rh7+2qs3bwk+}kWi?@?+}<+t7e&JJ?>`6liJkH0du^VKN$);$Jp3qOMg z`;iWk5IWCDgA#stzipJ%`MQFUN#{>Jto3mq%olMDVDnr{O{=hD*6huGF4mxok#=A9 zf2Ux({3vNX^x)GWko{pEOH} z;J}?Or+K_Er-M{ih}qHKE6?LD<5UNfwsNBus4(iEPoBhlp#>HT=HC#JD(2MRh4%MX zQGMQk4u5u!5e4+3AY6E`z>lU5jKGis4P@6T)h zt-e-^`ThOjDA!UL7O8z!7)zQe6k2q7|LgG^G4VOFeN}5eJI_sCc z>Jr9cG}jttc7ZZOGf#Z;`W%&th9UNEy1I=FN;gwqN%=kej`#{sa;^DDg^0biLSACG zoxB2z9J&7+mlNkvnVvGsWZhsiKqt)+gUjS+>hayIXD#Gsm)&7@6aSqYzjP1;C&X=_ z<}>sCHTAYbBaG?)gTKja?BxhtKmmGBp$D1A|Dh;MHbF@=jKXd$-mVDdeBB;Nn%nXH z4xh_N(0&Hivs|TE(OMXp$xc|NZ`12)`jb+N<1xb96C)zBqRn4UIR{}aL+wK`zM6!ZhGNV z*>4Z)aJU9M1`JHZVGCMArNtNBA~sX$#06uH7`rYmSW#-RLj~VE`6-%sA}ZWE@7q_h ze_a}zB*3JEoWV9vI?*@F;+r6rDhk;VNdB0P;QN?pAw`Wf zQ0V(S5(VYRQkm&`k&N6ly}EhIUO-trYG8@6$%@TM+ITuI>d4jzB=+V=mDdz#^i+2e z2*TfR6}U@49N15(v!bENdcQ*Y6t(3b%y^k{Sy;|XyiDu+;Jbfqxeg-^w>r^YV0h=C z3RW!NZGO^uI%Xr#Od2<-ixE#mX#(G4NTJL?!6g=zyRP$CQ%Jj5yiUKnj!9DRa)fz9*)WJL!S;YCeMOq9{0-*RK_xlK$94XzK?(^+HRTX7!#ZM{7Oj=V-YWIldw&!Mq_pH0T? z)&ykp8gGea%hyiVk9MM+o}=D_@jOS1Fr{UnY+Ny{wrgy>6(6UPrAQ(jw-GIDDs|8=*UG&lBNxAJ+MJnlvw@?KA4dEL35RSbfwX8#wQlLS}| zgw#Eoh&cpi1Hr5-nI}<|6FYCad$u+mb%9l0Re!Q0YF-&eXPsFKh5w{6=;8~n<9Ej# z$H%pc( z6P4twuBNHg)ghtV0m_3t%7Tg_f2F6gd=k+n39&&yudH`uAikTI(q2L8F*S*X7aX5t zHiaxt#MlVdGUn~+uKO~^tST~hf`~#v18K@fs+kRAUdtYVNJlp@Ay!evygqG4HH$mR zkVq}PYsK{<8&$ILtn(w|c}Qx?kfX`=xOYB}ne%E}DW~P2hGxNh`kNe<@6&X6H*;6` zAbVLx%q`QVS{6BoS_krdA!LBQ72Uv2LSvNFWbwv1FpBg#`2rPG6f^Ltj3};d970<}WP28E9J#G-g=F!VjCZ zOoc^Y7n5Bo__}NQu%sfWtCkpWWkbf-(q{w0$NpYK-u=O2&GoZ0qq3E{i%2duK9~Cp zZ6@{H%WRt@;D8?t)4~bUcIgCmVuaA$f57kwdl~QL!j+KFOazjH#yK1t7 z!f*3%XBy55~ifAHPhpt+W3?KtQnIe=c9r(=+Uv2tp<0vf)rlW>KQ= zR3&D&tW}{igH1;|jOCp4wS5|3&3=wSZ1Zov|Ex)&A{9(P_g6S3=t2cJV+9)cI&yA0|^ zHYEbfv|NT@k{vhq#7|c0n%3)43v6_n7zz3v@Q%rqZ$6fM>|vp4aZIyAKvPw} zF~C$#9x&w&7RVIXyg#It@v#zME-5c3wOrLKv`q7KK;V|ZqFSB7Q-1%FhZ~*AZE2wW zW57P$ro^D^>Hxf}ZBV#QWM7Q3Qaj$*zcK+#Jj*>@CsOCoB>MLm95Ad|=t#lTMISyn zIVmQeOHI?CmPfpUu7kz4YQ>KKgDn^j#_(g&s$ogY3hd+KM&^ zpE3maHoL0|vC%a;n7jGetsfoglo9(0qNfDVwnGNs!vCbp1Oz^ zQZHAfZJaq=%K-^LFq_T<)~Jm;f20i!54)Z%R>VSz>mgoonhG_9IE zdp6g}cnb)vRwiCSs>{1!Qg!s5%aRi(_uW2_a$L&BKz&b{5;dH#A#RR7Y^AW5q?cdz zZ^b_Vk>-AH9L9rpwt0Q*9;3`pmMS<~z5YtPLfJeg#y%Xg>W z=NpM=D9F;00E8>oaevpW&%s`RE?VA(iAl11J1Wd`i)l$khl~WEU{$+y43>6P$JMHVqE2*NdwzEQ>+^|=hHFc5HAH-j)x_#t7 zEif4iVv4*-{uv8Wx!|p08rH2<;j=@V&5g|g5G?f`x1Tjv{Dd=0jd?AXefy%QgmBqK zCT^paJDqj>!L?A48;{zrw$J-^#yX6Wl;iT+cDaOQhNZ35zY`V7x0GTar%=$)nk*M6 zXK(fI?q>z#hy)3-x16KLs$+0p_NlfQcBVX@>lYq_GUU~6UiaJB00_C8z^>>RmH}9> zIL`BxTQLk39c58ZS;zm(QW{Jss?yBC^%Im-5vMY8{i+KJ%Up-{x8!(*j9)PAsikXiz&`RLNaj4=KOC8I4ki1j+N*8w3ehG zaQdVmFVB%72UM1TE^GHi7h5j0{hodkB;FAY*$rd!XbLx`z* zJ?Csn=tW^7yib9dDc#o}{aDSs-N<@vjE$!n#;_S(V z|LHOAqHkE@Jd&ph3*T1p-`=F#Ye@B#jKO6oK7LU6WrZPJ(Xl#fHtTs&geL!(qXRZ| z?*r^2oSd9gQBmR7#b>D(*_NrTdIf|(92*b9C5-2>JI-XJI zHlar13A>Y;ydD_huicxKR0OT>iSVyK5~Fh7!n~$yDm|oPVZT_a%#yOi@DaAf5g+`A8QJb0%=>O2Q(~llVi0V7!gD*5W_a{L=)}moGd%LEr=S%KBE*Br@mT!7ROa0%> zBjpMbcbNf|YT8B7i^%eM_0T^~FJ+Oq;o6fmq4YeGDMKQM(2*gSfZ`@ybT(W#3F z()?O>Hz(%#q2jJMJx*l(i%Cg7{ryT78VC4CqH*5OR^G{nz`!D^XHQa?UO)NjHg;*Q z>jc0F?W?*BWPy*s+F#zihUL$Z*h{R(hY(Oa|MY4J8%$%$t) z5d*~7Z()EWNqS@t4Lj2+#_n644^6OD*s8I=*J&(Ik{o9ZhQh5yRE=S4DOjzu?Jgvpa;) zIAuvVsD`;rb%A&jZww2p0qgJx=Iz9xj_!S>cp#n?<1)a<(@4pC{sLLFuv5aygO_Qq zIh9VpTVOPZ6tyt!Vr~a(LSvY5_2~#FCa)dLizbO+r8K+CnA27|EqaMna=xshH7+F^`_Mbk)Nw5E3$btvy0BZdWO=F1%Gn3RE% zOk;6rvBnytGSOVOG??FK^5_IO5SO~kI z9Is{^>x256R#`8EQp>{(xDExT#`dX@^4@ zenBvc1l53y(K=@q$$qK-rA z+4{6+cp=!x*(3NNY06GD_&CVu(Ikiyyj+U12Zap$3^jujlkK$Zg^u-D3=V3LYn0ca zdpvMx*+-C&h?n@&_g312bV64LWz$*xf-SY2;EsHx4wfggW0Ij$4DlAbdhcBoqd$^-w^ z(w3+f0a$O=p~=U;nUMI^ig7LK+}}hR`FPLkqNSwCta;x;oM+(XFBOcc_?4A$5>X!l z9XwZ=Gw8*gG*Xf`XwUOKgE1j+Gt)(ZrKEzc%5_^Ll%p;3`a1aT2%WSYHLhyuWf&i} z!2n+tvYFeTSDJU}vm-`?StPcBuCIQ8On52a0DP&8T4BYJow6^Uq6 zmxNP4W968-5!flLGPiQo@F5vpG+Z7Nx?jnOEpF290rA0mXX4d_cVNsTG~`Ys>n)U65815{2l| zE6akBx~PiXA4ih!=y_x3o}=bTSS*e*iPL#=!X_A)o`UdXWmX?lx!27x=PcNqo+BIN zF=tgGW%radS6Q(_%MGX1=u^91g1SLNui-rTAF~Kc%oL~4e=2pmi!w!~vg>9K{@~YF zjb_ADxv5G8BzLq6aWy;?EF@knUEL+1)*<#D<`eVOpo}Wty}Uq0Hf<8z^#k}5Di*o*B2Kk;ZOtyX?b~hpR@1oSlZ*` zV}PAh_&usY#iYK_39L{-3)?Z&u(cR)JOC*{h0*bGzO*Dc(kAnh zqax6^yMh+x5F{sYx9cwC4_b@CDuxpOaV13N;=2Ah4*$UY91=R|e?$hr!a)7shU(zo zy#INh%jFdB>(-8965=YmL)<5a-Ku|9%Y;5coFHo2kVAqpj^j||m(Kb{{G@Jtet_8z9<>f~J zkQUes_eA)upp*gi87d|cr<72Ms`7HQ*CN0nv75T|I{p1x-A^{Y?8e`d0)$LB%c$x} z027zKdLmPZ2p=ChZ=2iovnig@>-bj<0QseY(7mbwYh?{assmhrhd_;JmeGr6K`Y$i zR#UtPE{bVqEDkUj45~gA5;vg%`}_8NB>+B`mgBUeD>zEp5wmy@I2Y8)reXhP)DPo- z=q3opYL)0bRup+LVEQERgUyh5BYa{BlAv7#zLyv-b|#F9-CM2#EnY$)(|TFCxZYWf zV)p2BB*tDYPYD{|-p@vBl`&V}m(oKS^R;+F_b>wq*($#DIm=f}8;58?%t5&~)Gbps zY0Htdl*IX(dhkD#hv~+EpL`OW$QE@s>DH%J8-v$Oi8Hx7i~QJOA9LLfJ!}s|73zM6-v~kA&i-<#XM=C z`Oj(<|3}Nq%4E}8kck+l%DCE9?RheTXPQ-DMB=6O68$_4~uzT;W8Zyq7zf~-PJGq||pczSijOg{dQ_AG&kUQWZg`&RQU2S};qP4EgqmnYX1|T$PR%5(;jLe<>04F2>vOQP8u|5Dn?y>{ zFee$fquC?9W5jvV$mvVe$`Q>nd!PFjjI2XgDgFmzB(h@ogpWfTmt#c^;UP*t`G*zZ zH)fANXi~Ee#DBE}Jg#vrx~y}$4BGV6!dKyD?J}Y_`$=+vtU$z=a&CxZ`ZeIvi~!QirUoGHdiF^3oTT{vtYj*dro zgpSNY#bRDVyVwQE`xZ)z$|tL6p{1S*EiUTlE>lJh>UvIIJS9H)Vp#0z-b0^j3ft9U zybu~%7Bh3ne)c&F$5*srF}Z+V$2MuEXfC}@Zb`FAYYF3!{Q2cZ)rr%N z4LQNe(lR+oQ}a6w)L)XIBd@y48a6GW)?nZmiLbgzNVCv!)D?tkj&W^AN3s38`ULv@ zJ2L9C>&RHBN~rW)MeB7Og&_1P5cw^_n;&n6p#jx}F@G6;G8qiU z;o-TIkKvXG{-U^Bk(}(F)U^B2%F-(7yTmGE8utv(3k43fth;Bu77B7TihcGLLKGAf zUL+ifL|wC5T4r0%8`%W-_}0fpi6KIzFC-q)bcOgsO+mqY+3!AU*sgyUOm*THtvYCo zBJ&utv2v~up75woqN z$0Qy8Z3>fX2p>4r*_L*O*0_T`P-TV0#DMjnJrXR5ZHtWUOgUPnv!s=jke#Oa$ucNY zjiH+rqh`;twI@X*0xAVf@?O$O&qzy9)fgbS7G=e4k$W_9~o2-GgD#SH1V(rUhAR$ZSIQc)IfAtzZ?# z@^0nJsCSb;Hz2Bn=qYX}0)yeIxU<9qjr#l`Vm(Ep;&PAT{-=_B}I z$yJIj?SAW0Svif{^zU61T0Xldd}(7pyUPD;bCm0~x5b+FZ(Jh|S%W5%wVtO(ay#{T zY=U^(oD~mY_d{^N&fsWj$63!xkd);gj)4aRdk5uQ&HY$Yr%ub&a0^{_v81AN8C{Ox z;dGdS(RWdkp3#7$;h`OzBO?6mGpor-y$5?rI)O6HGK=t&!@~)(c(o+a%TbmuxL$`l zE%~(Bm(!Lhh1QZ|>n*;0KL(9-t{%z{CjA>kRDT#|=?o#T1~!uIIuS`}$)cL~(QYC{;O=`}%;)$X9STp(2a}UAt)=>+wrh9^crh zMXwAl)jQzUg^ajY_PVJ1OtAl2ErcE?PsYFf&(L|>$sylm2bjfXuF$v$M`UGlFuJez znQ|uhN{tR{OYj~xvj>MzM}gh;uXZZp{&v1Yta7BkWqA}n1vV*Oq!Dx39tv7DzJ76O ziXQQ!!D!8!P{!GyKw^JCqUw*>=g;eSbrS&>r^99+Dr)_&E_e#s!5@@`U2cbl_wJsd z)P#?4mjL^>u;g&iQiUzfE=T@GyL!pkcXyq4E)^a}%VXQOac~)+P;O=$ZUt#&+RBO< z?TKrja_!m=XNey6wce4L{9JTGO|0`|8CZ-WIxn#1`HF>QFV)yzwQN2o;IU=tlTowN zOMLyOFFRV-xVbYHm3}t+8w9}80R^A!ZF58w%kotG8kWbSu;1mkI=liBU&I8Lkusvq z!JCJ4(b_qDeF_fS+M29PuiXQ)!?mG1=U~qJmhEx6o6L7!-lIoSP3?%a4#HNwg4vg# zCHH|?Fy1GT4zttHO2s>|fZOfd$rPNozid;Rpipm(S8VN1$8dq1NYPd=HGn<(gL-&@ z%t~aTbVu#Mrj@Ea|RwSw};_ZEK0QYUS{WsTwHF(%^yLLv4%Aw&VvDWVfUr@ z_0onx@)BUpg%ikH;pAg z$cxPe4!f<{$xv%=o5JdBzd!7rqIniaBkXe3M>ffKKKnfi-VVRHd0FKsMC5bWYXf=m zJ0CUBLMK^R-E+j`$aZVYxH`P>SVs{(Zd&)h|H~4NZvM`ipi6n$Px|hHIvB0-eQD$f z`upbQ(_*g?b(lu^@J9yWxD#uqq%1MFpTkqn$K@ur@I|m`9Eyd#^CSh^?b;o$%BNLr z%d#NRq?MM)%~-mGAjM1C6Mcrfu{-p8p^d*{`MC75banECWTMMmTXhwl4f z=%%tL2@%a#BMLdu;9#&o42K*@65IQ#U@?VLw{3fVF~{#XSN65`-b)6J#|L&#X)<~X3iS&AgLC7RbrcT|Hr?3uCA_t@@uTAd3`a;1$f!Fn}zyP+nGV_mK;8a zc|iQ`tlgeZB~^@|&vdVQZPHhigFT+MVl~9}V>DG64?~P$@-ROfyrN{RqBBu{%#SJB za8#cE=C9yFo7JL#tzQmvXMY0Cp9tQ?I3bYk!558yx4C`pzE5LTb@s#7(D``XOIW3HxhMR!IXTw10>O~(mg zW>HK=@fJn%*4%o(gs}uj-elJ9(22Q~tivuTxAICgb2g1`lHMVke;4Pn`R!x8H}X{( zCjFX{kKqZOuEh5YnE?yEU|XJ*L!mZcRx3AEjHbA}^V9ujCNry(kwoMOD};~IQF%!@ z{#PedJuT~8MprXmvyWrHc7GI94-rt$w@E;?=LhK35b= zzQW`g|CooM<9G<|^XUD;iDswgAW%x~ey6v(3bCM(SF(x_LH}E989{a~b?mTa-<@<1 zmO4uogy{HBkIea0`)Y`sJ3! zQ*EdzwuCC7cDh}%6cu+8qe5QsWy}TFs9e)eN=WvOgKf3JMka8kRl*38mKzL87l;NU z>bI%O?cN6>;Yvsyt%6k4v$ZT*&Ye&rUncJyfI47l{CY7dstrcEl4pemBC9UbK8|>p zYfEUaWQA|Xvs$8$VyK7wqB+ce$U|!Q!+jy`%KP;cTwltBJCtauM{nlVDQ7Tp^*B*u z3hG^;)kB!lCdKpVhDM)qou(w&!VK{liD8#6oEFEFGa5w;Z|r4!wWb&~8p$PgwqlW8 z7H_Fgu;`z^>v3(P+GLwH#M!Tf@KXiS@3=3{xT0y7unuqh7S@`bLHJQ z4LhlvSZwfR-$4$zZ0!V@>xuc__AjH#7R$QPZTav6Y5{s#QC>YNY zTtWHfX4~qIy`o#Gfe*FM+E|a=l3IfPQmv-GrudJl%(UH)s@N>}<~C~nDx>_u=PxXn zBINVk5u%C7lv5-u^4zgK?}`c)blwnve%4jI9EidnBb5NRPt3$WpclaGatvsjqTKcua%*3flZ6;1z88_xEt z_Gi!}+9AP&11Y;rtZAv0GEOS_I*My`1sJBs_39noSZn|6cX+F%rD?Eo<-CWFHbK7b z=U`;Q>L!v8mm#KrbvLA|mt2$;pgM(A(#6rA?2h9;ZT)SE0=0LWJZ|={D->@a_dc_z zG;H}Tc|h(zDo|GbS_bb&#Lx|mf=5Nv4rWEVyiom-ufLemMr|z;^#i(vJVjG!gua?| zr%s>Gdltjt-54vUx8c7InDyfbC&K3%U!G*MuRw zm{|7sX7|n`wKGbyfCpwPQbQkJN>qkKSpM+?P0mkDjz#y+cH74S##!`Iy{LY5;CuSY z)eRtttF4SF{%Aj!#IRv<^oj2?$o4V7Jb^r>YPlufPQTl`qse^w|~ zP9K~;Y=JiwaS>@vs`K5odlPnKr(&pCkOxZC-I==&|5D1;dA%~(tzN_n(dj5O`X;2c z81RApe9v)F1ksDq+)G!XbRzO22W*XFcc`iIGPu>L#lS6cXlv7az~eW3z_sX;pvszN zfd5}t%5X9k`^U>k$rO!};9$v~dT*L>==Zwgy#g3ZdsF}Os|>zBWn_aN{!SW2tC?z+ z(7mP>Le#-*e`dpF!X93GhT{v_F4^Jj#8Gg=GO1}vsqrbPlNwxi>{>6WW?ER3l5KtW zzlq%J){vZfL>g8=i*!J;4B?*e=!0+Vo0sj(QXx*7Jqx1GOCXQqGW;p#V13tLU*5V{ zoOakB&C)PdCgmNPVYRTY{OCgAPq4}Io)t_iOllW$BDUxeDVNu{`HJe@RS*)mWMH0@ zNbl^DQ>88z^#DTam*c$}9$mnHkL!2a+c&8neFbq&$|n82yMMzJy-F%}ff%RzB1VaE zcNkPvQS|~7he&9uU407`T~3YdZT}9N+(>8Q5LO#Je6aBw*d1+ezgGa)XfCy`4w?gU zwg~q)6;4xQHmoBVKY4Y@&+0pH@L+s%vyjUx1vQ=g+pgE>DhK`Odc>}cVi-}$iKwJ@ zE?9Q+4mHF<5(y+_ zg>0=vHp0x&#!Yr|RNW6k;7>6c>q9@cR`w?2G$6Pw?i-zz5AU?xevr-GKk9-UVz zF4%t}hGt)?`RQ?NSyVaIV=<367F~Ir_E5$Kwe51DeRc&)W4zl~8{7AC=Ugucm1Obn zt9y?eSrU9|3&n_MxTnHdVJ2vY#e(hdp_^M@6#tClHHLHQlTH(1t;aL};na|N!BU5*>!`X?dQp@K?WCV&vR;+AExjw-N~cJYR#QMhmy3H{2n#kw z!Ffg7#;#-%fuC>9;IZYIxGVk#?MdPP|Z8R8liS_FmJ`-7Cl|>z30i zgXcM`Y!NJISva3HoJ-ILJTEriR5}$vuDrRJc^j%w0742f69CYl{nWRGTDGFv#&72r zZ{Jui4J_`p5w4%i-3YW{8f|1+s(d?_r-8ho4ADa8P2{-HilX>Rm#J1NkTHFn<}M*u z(IlY=&73uJ*9_y7%?h|-_(NOxM`1zv+Fh8b+mu_c8{Ii3PT+u-@+A8Zz4%R*(jWi+ z?+sdMCvDO3-T_=o2Dx(uP$Qf(LPZiS(%HMe8~iGRAbpx?N-nO3GJZl`^W?N|&J9X? zdasZZ59+^j$oGc?dHsuHLuRlh^4A~wevadXu#2s0kqD`Z=edhQx*(d304LzZPhZ46nNq!4mdj-399{O31Xh=AV~b zPkQRDmTd>5@+;3!{8=5eKO_U9qIe(=V-rzNSZ{DwQ(Ems)ZW0=!MfxASIE=7sa#UW z+8mI0{j1GziJOP07w9y8s~GY#wR>GGxR;pM5%EXCr=bR8wBCBd=GYx*Lb5oY+9D4+72c?KlJ$iGJ|ucu>MA%K*XC5LAf7tElKQ`{k8 z@4%h*$fQWN-Hkp+cDm~Pc;1Yj(%A_Hs0(#d7p0i*?FlAFdg0S83@=6+wc>e;UEm8M z;$>#V^c6_Z^-gJ#rBsj``bOHb6H?y`0GOcBR}&Hviu+)rrPOBNZLcAtkyiHu$x}xe z6J$_iup7rVCn|5sevhlKRUiuR<=(vs#8sA%kg%~~Ev6GSiuXZNfpoo~5&j(5`b(UC z5&Z=R021kuc}crmky;kT-Oz(p{zt+$jWP(`C-grQ4afmw|2L!g--j3;$^Td#KyXUH z#P;YweEPo*b4UcOhW>vxS7P;lcBqZN{e)zUw?=ihrX2;8@*~MXl-^fLngstpz*|K= literal 0 HcmV?d00001 diff --git a/droid-help/src/main/resources/Images/Export dialog.png b/droid-help/src/main/resources/Images/Export dialog.png index 3e60ee79e83e0cc49fbf9b911e3bc1f2ab11b885..20895ee0b5be0cd4c37c530295b9d248365daefb 100644 GIT binary patch literal 89418 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

V83lxID`Ul|R+HnCn73`xf9)V>8j$ooV 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 LPrc%{b=W@ukBD0V literal 21212 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 diff --git a/droid-help/src/main/resources/Web pages/Command line control.html b/droid-help/src/main/resources/Web pages/Command line control.html index 938290f02..f62f4c571 100644 --- a/droid-help/src/main/resources/Web pages/Command line control.html +++ b/droid-help/src/main/resources/Web pages/Command line control.html @@ -97,7 +97,7 @@

Filtering results once identified
  • - List filtering options + List filtering options
  • Specifying the signature files to use @@ -216,7 +216,7 @@

    Format identification

    - Writing to a file

    + Writing to a file
  • To output the CSV results to a file rather than the console, you can use the -o option: @@ -534,6 +534,28 @@

    Exporting profiles to CSV

    +

    + If you want to export a subset of columns, simply specify a space separated list of those columns after the -co option: + + + + +
    +  droid -p "C:\Results\myprofile1.droid" -e "C:\Exports\myprofiles.csv" -co FILE_PATH STATUS HASH PUID + +     +
    +

    + If you have defined an export template, simply specify the absolute path to the export template after the -et option: + + + + +
    +  droid -p "C:\Results\myprofile1.droid" -e "C:\Exports\myprofiles.csv" -et "C:\Templates\MyExport.template" + +     +

    A final option for export is to control whether a Byte Order Mark (BOM) is written at the beginning of the file. To write a BOM, use the -B option: @@ -1223,6 +1245,15 @@

    Command options

    droid “C:\Files\A Folder” "C:\Files\A File” -co FILE_PATH STATUS HASH PUID + + -et + --export-template + Absolute path to the export template + Specifies that CSV output should be written using the customisations defined in the Export Template. + + droid “C:\Files\A Folder” "C:\Files\A File” -et "C:\Templates\MyExport.template" + + -e --export-file diff --git a/droid-help/src/main/resources/Web pages/Exporting profiles.html b/droid-help/src/main/resources/Web pages/Exporting profiles.html index 504324105..4ab16af2d 100644 --- a/droid-help/src/main/resources/Web pages/Exporting profiles.html +++ b/droid-help/src/main/resources/Web pages/Exporting profiles.html @@ -75,37 +75,34 @@
    export a profile, press the    Export   button, or select the File / Export - all... menu item.  This will bring up the export dialog window: -

    -

    - + all... menu item. This shows the export dialog. The profiles you have open are listed + in the export window.  If a profile is empty, or in the process of running, it is greyed out. +  Select all the profiles you want to export into a single CSV file by checking the boxes next to + them.   If any of your profiles have active filters, then + the results will also be filtered.  Each profile can have different filters defined and enabled.

    - The profiles you have open are listed in the export window.  If a profile is empty, or - in the process of running, it will be greyed out.  Select all the profiles you want to - export into a single CSV file by checking the boxes next to them.   If any of your - profiles have active filters, then the results will - also be filtered.  Each profile can have different filters defined and enabled. + There are two possible ways of exporting the profiles. +

    - You can choose which columns to be included in the export as well as whether the values - should be enclosed in quotes when exporting. You can also select whether the export - should produce one row per file, or one row per format.  When exporting one row per - file, each row in the CSV file will represent a single file, folder or archival file - profiled with DROID.  If exporting one row per format, each row in the CSV file will be - a single format identificaiton made by DROID. Since a file can be identified as being more - than one possible format, this option will produce CSV files with multiple rows for the - same file (but with different identifications for it). +

    + Selecting the columns for export +

    - The characters of the export will be encoded as UTF-8 by default. If you need to set this - to the encoding used on your local machine instead, select 'Platform specific' instead. -

    -

    - When you are happy you want to export your profiles, press the    Export - profiles...   button.  This will bring up a standard file-save - dialog, in which you can specify where you want your CSV file to be saved.   + When the "Use export template" checkbox is unchecked, you are presented with an option to select + one or more columns that you wish to export, as shown below: +

    +

    +

    CSV File Columns @@ -248,11 +245,11 @@

    The date and time on which a resource was last modified.

    - MD5, SHA1, or SHA256 hash + MD5, SHA1, SHA256 or SHA512 hash

    If you have enabled hash generation in - the preferences, then this column will contain the MD5, SHA1, or SHA256 hash for each file and archival file + the preferences, then this column will contain the MD5, SHA1, SHA256 or SHA512 hash for each file and archival file processed.  See "Detecting duplicate files" for more information on hashes.

    @@ -298,7 +295,70 @@

    blank even when a file has a PUID.

    -   +

    + Selecting a predefined Export Template for export +

    +

    +

    + If you have configured any export templates for use with DROID, the "Use export template" checkbox + is enabled. Checking this checkbox shows you a view to select one of the configured templates + to be used for export as shown below: +

    +

    + +

    +

    + An export template is a simple text file, with a .template extension, which defines customisations of + columns to be exported. Using a template, you can customise headers for the data columns, add new columns + to the export, convert the data in a column to be uppercase / lowercase and change the order in which columns + appear in the export. You can make an export template available to Droid by copying it into the + ".droid6\export_templates" folder. An example template is shown below: +

    +

    +

    +        version 1.0
    +        Identifier: $ID
    +        LowerName: LCASE($NAME)
    +        Language: "Simplified English"
    +        Submitter:
    +      
    +

    +

    + The above template indicates: +

      +
    1. The template is defined in version 1.0 of template syntax
    2. +
    3. There are 4 columns to be exported in the order Identifier, LowerName, Language and Submitter
    4. +
    5. Values in the column "Identifier" are populated using values in the "ID" column
    6. +
    7. Values in the columns "LowerName" are populated by converting the values from the "NAME" column to lowercase
    8. +
    9. There is a non-profile column called "Language" and the values are hardcoded to "Simplified English"
    10. +
    11. There is a non-profile column called "Submitter" and the values are left empty
    12. +
    +

    +

    +

    + Other options +

    +

    +

    + Once you have selected the columns to export or selected a template that you wish to use, + you can also select other options such as whether the values should be enclosed in quotes + when exporting. You can also select whether the export should produce one row per file, + or one row per format.  When exporting one row per file, each row in the CSV file will + represent a single file, folder or archival file profiled with DROID.  If exporting one + row per format, each row in the CSV file will be a single format identificaiton made by + DROID. Since a file can be identified as being more than one possible format, this option + will produce CSV files with multiple rows for the same file (but with different identifications + for it). +

    +

    + The characters of the export will be encoded as UTF-8 by default. If you need to set this + to the encoding used on your local machine instead, select 'Platform specific' instead. +

    +

    + When you are happy you want to export your profiles, press the    Export + profiles...   button.  This will bring up a standard file-save + dialog, in which you can specify where you want your CSV file to be saved.  

    diff --git a/droid-results/src/main/java/uk/gov/nationalarchives/droid/profile/CsvItemWriter.java b/droid-results/src/main/java/uk/gov/nationalarchives/droid/profile/CsvItemWriter.java index 5726d5e23..0352df300 100644 --- a/droid-results/src/main/java/uk/gov/nationalarchives/droid/profile/CsvItemWriter.java +++ b/droid-results/src/main/java/uk/gov/nationalarchives/droid/profile/CsvItemWriter.java @@ -35,30 +35,22 @@ import com.univocity.parsers.csv.CsvFormat; import com.univocity.parsers.csv.CsvWriter; import com.univocity.parsers.csv.CsvWriterSettings; -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.lang.time.DateFormatUtils; -import org.apache.commons.lang.time.FastDateFormat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import uk.gov.nationalarchives.droid.core.interfaces.config.DroidGlobalConfig; -import uk.gov.nationalarchives.droid.core.interfaces.util.DroidUrlFormat; import uk.gov.nationalarchives.droid.export.interfaces.ExportOptions; +import uk.gov.nationalarchives.droid.export.interfaces.ExportTemplate; +import uk.gov.nationalarchives.droid.export.interfaces.ExportTemplateColumnDef; import uk.gov.nationalarchives.droid.export.interfaces.ItemWriter; -import uk.gov.nationalarchives.droid.profile.referencedata.Format; +import uk.gov.nationalarchives.droid.profile.datawriter.DataWriterProvider; +import uk.gov.nationalarchives.droid.profile.datawriter.FormattedDataWriter; -import java.io.File; import java.io.Writer; -import java.net.URI; -import java.nio.file.Paths; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; -import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -68,90 +60,17 @@ */ public class CsvItemWriter implements ItemWriter { - private static final String HEADER_NAME_ID = "ID"; - private static final String HEADER_NAME_PARENT_ID = "PARENT_ID"; - private static final String HEADER_NAME_URI = "URI"; - private static final String HEADER_NAME_FILE_PATH = "FILE_PATH"; - private static final String HEADER_NAME_NAME = "NAME"; - private static final String HEADER_NAME_METHOD = "METHOD"; - private static final String HEADER_NAME_STATUS = "STATUS"; - private static final String HEADER_NAME_SIZE = "SIZE"; - private static final String HEADER_NAME_TYPE = "TYPE"; - private static final String HEADER_NAME_EXT = "EXT"; - private static final String HEADER_NAME_LAST_MODIFIED = "LAST_MODIFIED"; - private static final String HEADER_NAME_EXTENSION_MISMATCH = "EXTENSION_MISMATCH"; - private static final String HEADER_NAME_HASH = "HASH"; - private static final String HEADER_NAME_FORMAT_COUNT = "FORMAT_COUNT"; - private static final String HEADER_NAME_PUID = "PUID"; - private static final String HEADER_NAME_MIME_TYPE = "MIME_TYPE"; - private static final String HEADER_NAME_FORMAT_NAME = "FORMAT_NAME"; - private static final String HEADER_NAME_FORMAT_VERSION = "FORMAT_VERSION"; - - /** - * Headers used in the CSV output - */ - static final String[] HEADERS = { - HEADER_NAME_ID, - HEADER_NAME_PARENT_ID, - HEADER_NAME_URI, - HEADER_NAME_FILE_PATH, - HEADER_NAME_NAME, - HEADER_NAME_METHOD, - HEADER_NAME_STATUS, - HEADER_NAME_SIZE, - HEADER_NAME_TYPE, - HEADER_NAME_EXT, - HEADER_NAME_LAST_MODIFIED, - HEADER_NAME_EXTENSION_MISMATCH, - HEADER_NAME_HASH, - HEADER_NAME_FORMAT_COUNT, - HEADER_NAME_PUID, - HEADER_NAME_MIME_TYPE, - HEADER_NAME_FORMAT_NAME, - HEADER_NAME_FORMAT_VERSION, - }; - - /** - * List of headers that appear more than once if a file matches more than one format - */ - static final List PER_FORMAT_HEADERS = Arrays.asList( - HEADER_NAME_PUID, HEADER_NAME_MIME_TYPE, HEADER_NAME_FORMAT_NAME, HEADER_NAME_FORMAT_VERSION); - - private static final String FILE_URI_SCHEME = "file"; - - /* - * Indexes of the headers used in the CSV output. - */ - private static final int ID_ARRAY_INDEX = 0; - private static final int PARENT_ID_ARRAY_INDEX = 1; - private static final int URI_ARRAY_INDEX = 2; - private static final int FILE_PATH_ARRAY_INDEX = 3; - private static final int FILE_NAME_ARRAY_INDEX = 4; - private static final int ID_METHOD_ARRAY_INDEX = 5; - private static final int STATUS_ARRAY_INDEX = 6; - private static final int SIZE_ARRAY_INDEX = 7; - private static final int RESOURCE_ARRAY_INDEX = 8; - private static final int EXTENSION_ARRAY_INDEX = 9; - private static final int LAST_MODIFIED_ARRAY_INDEX = 10; - private static final int EXTENSION_MISMATCH_ARRAY_INDEX = 11; - private static final int HASH_ARRAY_INDEX = 12; - private static final int ID_COUNT_ARRAY_INDEX = 13; - private static final int PUID_ARRAY_INDEX = 14; - private static final int MIME_TYPE_ARRAY_INDEX = 15; - private static final int FORMAT_NAME_ARRAY_INDEX = 16; - private static final int FORMAT_VERSION_ARRAY_INDEX = 17; private static final String BLANK_SPACE_DELIMITER = " "; + private final Map columnsToWriteMap = new HashMap<>(); private final Logger log = LoggerFactory.getLogger(getClass()); private CsvWriter csvWriter; - private final FastDateFormat dateFormat = DateFormatUtils.ISO_DATETIME_FORMAT; private ExportOptions options = ExportOptions.ONE_ROW_PER_FILE; - private String[] headers; + private String[] allHeaders; private boolean quoteAllFields; - private final boolean[] columnsToWrite; - private int numColumnsToWrite; + private ExportTemplate exportTemplate; /** * Empty bean constructor. @@ -167,73 +86,69 @@ public CsvItemWriter() { public CsvItemWriter(CsvWriter writer) { this.csvWriter = writer; this.quoteAllFields = true; - numColumnsToWrite = HEADERS.length; - columnsToWrite = new boolean[HEADERS.length]; - Arrays.fill(columnsToWrite, true); + populateDefaultColumnsToWrite(); + } + + private void populateDefaultColumnsToWrite() { + columnsToWriteMap.put(CsvWriterConstants.HEADER_NAME_ID, true); + columnsToWriteMap.put(CsvWriterConstants.HEADER_NAME_PARENT_ID, true); + columnsToWriteMap.put(CsvWriterConstants.HEADER_NAME_URI, true); + columnsToWriteMap.put(CsvWriterConstants.HEADER_NAME_FILE_PATH, true); + columnsToWriteMap.put(CsvWriterConstants.HEADER_NAME_NAME, true); + columnsToWriteMap.put(CsvWriterConstants.HEADER_NAME_METHOD, true); + columnsToWriteMap.put(CsvWriterConstants.HEADER_NAME_STATUS, true); + columnsToWriteMap.put(CsvWriterConstants.HEADER_NAME_SIZE, true); + columnsToWriteMap.put(CsvWriterConstants.HEADER_NAME_TYPE, true); + columnsToWriteMap.put(CsvWriterConstants.HEADER_NAME_EXT, true); + columnsToWriteMap.put(CsvWriterConstants.HEADER_NAME_LAST_MODIFIED, true); + columnsToWriteMap.put(CsvWriterConstants.HEADER_NAME_EXTENSION_MISMATCH, true); + columnsToWriteMap.put(CsvWriterConstants.HEADER_NAME_HASH, true); + columnsToWriteMap.put(CsvWriterConstants.HEADER_NAME_FORMAT_COUNT, true); + columnsToWriteMap.put(CsvWriterConstants.HEADER_NAME_PUID, true); + columnsToWriteMap.put(CsvWriterConstants.HEADER_NAME_MIME_TYPE, true); + columnsToWriteMap.put(CsvWriterConstants.HEADER_NAME_FORMAT_NAME, true); + columnsToWriteMap.put(CsvWriterConstants.HEADER_NAME_FORMAT_VERSION, true); } @Override public void write(List nodes) { + FormattedDataWriter dataWriter = DataWriterProvider.getDataWriter(columnsToWriteMap, exportTemplate); switch (options) { case ONE_ROW_PER_FILE: { - writeOneRowPerFile(nodes); + writeOneRowPerFile(nodes, dataWriter); break; } case ONE_ROW_PER_FORMAT: { - writeOneRowPerFormat(nodes); + writeOneRowPerFormat(nodes, dataWriter); break; } default: { //what? unknown option log.warn("Unable to handle ExportOptions = " + options + ", was there a new option created?"); options = ExportOptions.ONE_ROW_PER_FILE; - writeOneRowPerFile(nodes); + writeOneRowPerFile(nodes, dataWriter); } } } - private void writeOneRowPerFile(List nodes) { + private void writeOneRowPerFile(List nodes, FormattedDataWriter dataWriter) { if (csvWriter.getRecordCount() == 0) { - writeHeadersForOneRowPerFileExport(nodes); + dataWriter.writeHeadersForOneRowPerFile(nodes, allHeaders, csvWriter); } try { - for (ProfileResourceNode node : nodes) { - List nodeEntries = new ArrayList<>(); - addNodeColumns(nodeEntries, node); - for (Format format : node.getFormatIdentifications()) { - addColumn(nodeEntries, PUID_ARRAY_INDEX, format.getPuid()); - addColumn(nodeEntries, MIME_TYPE_ARRAY_INDEX, format.getMimeType()); - addColumn(nodeEntries, FORMAT_NAME_ARRAY_INDEX, format.getName()); - addColumn(nodeEntries, FORMAT_VERSION_ARRAY_INDEX, format.getVersion()); - } - csvWriter.writeRow(nodeEntries); - } - csvWriter.flush(); + dataWriter.writeDataRowsForOneRowPerFile(nodes, csvWriter); } catch (final TextWritingException e) { log.error(e.getRecordCharacters(), e); throw new RuntimeException(e.getMessage(), e); } } - - private void writeOneRowPerFormat(List nodes) { - List headersToWrite = Arrays.stream(getHeadersToWrite(headers)).collect(Collectors.toList()) ; + + private void writeOneRowPerFormat(List nodes, FormattedDataWriter dataWriter) { if (csvWriter.getRecordCount() == 0) { - csvWriter.writeHeaders(headersToWrite); + dataWriter.writeHeadersForOneRowPerFormat(nodes, allHeaders, csvWriter); } - try { - for (ProfileResourceNode node : nodes) { - for (Format format : node.getFormatIdentifications()) { - List nodeEntries = new ArrayList<>(); - addNodeColumns(nodeEntries, node); - addColumn(nodeEntries, PUID_ARRAY_INDEX, format.getPuid()); - addColumn(nodeEntries, MIME_TYPE_ARRAY_INDEX, format.getMimeType()); - addColumn(nodeEntries, FORMAT_NAME_ARRAY_INDEX, format.getName()); - addColumn(nodeEntries, FORMAT_VERSION_ARRAY_INDEX, format.getVersion()); - csvWriter.writeRow(nodeEntries); - } - } - csvWriter.flush(); + dataWriter.writeDataRowsForOneRowPerFormat(nodes, csvWriter); } catch (final TextWritingException e) { log.error(e.getRecordCharacters(), e); throw new RuntimeException(e.getMessage(), e); @@ -252,46 +167,15 @@ public void open(final Writer writer) { final CsvWriterSettings csvWriterSettings = new CsvWriterSettings(); csvWriterSettings.setQuoteAllFields(quoteAllFields); CsvFormat format = new CsvFormat(); - // following Unix convention on line separators as previously + // following Unix convention about line separators as previously format.setLineSeparator("\n"); csvWriterSettings.setFormat(format); csvWriter = new CsvWriter(writer, csvWriterSettings); - if (headers == null) { - headers = Arrays.copyOf(HEADERS, HEADERS.length) ; + if (allHeaders == null) { + allHeaders = Arrays.copyOf(CsvWriterConstants.HEADERS, CsvWriterConstants.HEADERS.length) ; } } - private void writeHeadersForOneRowPerFileExport(List nodes) { - - if (options != ExportOptions.ONE_ROW_PER_FILE) { - throw new RuntimeException("Unexpectedly called per file header creation. Unable to proceed"); - } - - List headersToWrite = Arrays.stream(getHeadersToWrite(headers)).collect(Collectors.toList()) ; - - //if we are writing one row per file, then we tag the "per format" fields as additional columns, - //if such columns need to be added, we create appropriate headers with a running suffix and write - //them to the file - if (!Collections.disjoint(headersToWrite, PER_FORMAT_HEADERS)) { - Optional maxIdentificationsOption = nodes.stream().map(ProfileResourceNode::getIdentificationCount).collect(Collectors.toList()).stream().filter(Objects::nonNull).max(Integer::compare); - int maxIdentifications = 0; - if (maxIdentificationsOption.isPresent()) { - maxIdentifications = maxIdentificationsOption.get(); - } - if (maxIdentifications > 1) { //add headers - for (int newColumnSuffix = 1; newColumnSuffix < maxIdentifications; newColumnSuffix++) { - //"PUID","MIME_TYPE","FORMAT_NAME","FORMAT_VERSION" - for (String headerEntry : PER_FORMAT_HEADERS) { - if (headersToWrite.contains(headerEntry)) { - headersToWrite.add(headerEntry + newColumnSuffix); - } - } - } - } - } - csvWriter.writeHeaders(headersToWrite); - } - @Override public void setOptions(ExportOptions options) { this.options = options; @@ -305,44 +189,6 @@ public void close() { csvWriter.close(); } - private static String nullSafeName(Enum value) { - return value == null ? "" : value.toString(); - } - - private static String nullSafeNumber(Number number) { - return number == null ? "" : number.toString(); - } - - private static String nullSafeDate(Date date, FastDateFormat format) { - return date == null ? "" : format.format(date); - } - - private static String toFileName(String name) { - return FilenameUtils.getName(name); - } - - private String toFilePath(URI uri) { - if (uri == null) { - log.warn("[URI not set]"); - return ""; - } - if (FILE_URI_SCHEME.equals(uri.getScheme())) { - return Paths.get(uri).toAbsolutePath().toString(); - } - - // for URIs that have other than "file" scheme - String result = java.net.URLDecoder.decode(uri.toString()).replaceAll("file://", ""); - result = result.replace("/", File.separator); - - // Handle substitution of 7z - final String sevenZedIdentifier = "sevenz:"; - if (result.startsWith(sevenZedIdentifier)) { - result = "7z:" + result.substring(sevenZedIdentifier.length()); - } - - return result; - } - /** * No config is needed by this class, but it's retained temporarily for backwards compatibility purposes. * @param config the config to set @@ -356,14 +202,28 @@ public void setConfig(DroidGlobalConfig config) { @Override public void setHeaders(Map headersToSet) { - if (this.headers == null) { - this.headers = Arrays.copyOf(HEADERS, HEADERS.length); + if (this.allHeaders == null) { + this.allHeaders = Arrays.copyOf(CsvWriterConstants.HEADERS, CsvWriterConstants.HEADERS.length); } // The header for hash is modified based on algorithm used to generate the hash String hashHeader = headersToSet.get("hash"); if (hashHeader != null) { - this.headers[HASH_ARRAY_INDEX] = hashHeader; + if (exportTemplate != null) { + Map columnPositions = exportTemplate.getColumnOrderMap(); + List> profileCols = columnPositions.entrySet() + .stream() + .filter(e -> e.getValue().getColumnType() == ExportTemplateColumnDef.ColumnType.ProfileResourceNode) + .collect(Collectors.toList()); + List> hashEntry = profileCols.stream() + .filter(entry -> entry.getValue().getOriginalColumnName().equals(CsvWriterConstants.HEADER_NAME_HASH)) + .collect(Collectors.toList()); + if (!hashEntry.isEmpty()) { + this.allHeaders[CsvWriterConstants.HASH_ARRAY_INDEX] = hashHeader; + } + } else { + this.allHeaders[CsvWriterConstants.HASH_ARRAY_INDEX] = hashHeader; + } } } @@ -376,26 +236,22 @@ public void setQuoteAllFields(boolean quoteAll) { public void setColumnsToWrite(String columnNames) { Set headersToWrite = getColumnsToWrite(columnNames); if (headersToWrite == null) { - Arrays.fill(columnsToWrite, true); - numColumnsToWrite = columnsToWrite.length; + populateDefaultColumnsToWrite(); } else { - int numberToWrite = 0; - for (int i = 0; i < HEADERS.length; i++) { - if (headersToWrite.contains(HEADERS[i])) { - columnsToWrite[i] = true; - numberToWrite++; - headersToWrite.remove(HEADERS[i]); + for (int i = 0; i < CsvWriterConstants.HEADERS.length; i++) { + String currentHeader = CsvWriterConstants.HEADERS[i]; + if (headersToWrite.contains(currentHeader)) { + columnsToWriteMap.put(currentHeader, true); + headersToWrite.remove(currentHeader); } else { - columnsToWrite[i] = false; + columnsToWriteMap.put(currentHeader, false); } } // Defence: if no valid column names were specified, log a warning then write them all out: - if (numberToWrite == 0) { - numColumnsToWrite = columnsToWrite.length; - Arrays.fill(columnsToWrite, true); + if (!columnsToWriteMap.containsValue(true)) { + populateDefaultColumnsToWrite(); log.warn("-co option: no CSV columns specified are valid, writing all columns: " + columnNames); } else { - numColumnsToWrite = numberToWrite; // If there are some columns specified left over, they aren't valid columns - log a warning: if (headersToWrite.size() > 0) { String invalidHeaders = String.join(BLANK_SPACE_DELIMITER, headersToWrite); @@ -405,6 +261,11 @@ public void setColumnsToWrite(String columnNames) { } } + @Override + public void setExportTemplate(ExportTemplate template) { + this.exportTemplate = template; + } + private Set getColumnsToWrite(String columnNames) { if (columnNames != null && !columnNames.isEmpty()) { String[] columns = columnNames.split(BLANK_SPACE_DELIMITER); @@ -414,43 +275,5 @@ private Set getColumnsToWrite(String columnNames) { } return null; } - - private String[] getHeadersToWrite(String[] headersToWrite) { - if (numColumnsToWrite < columnsToWrite.length) { - String[] newHeaders = new String[numColumnsToWrite]; - int newHeaderIndex = 0; - for (int i = 0; i < columnsToWrite.length; i++) { - if (columnsToWrite[i]) { - newHeaders[newHeaderIndex++] = headers[i]; - } - } - return newHeaders; - } - return headersToWrite; - } - - private void addNodeColumns(List row, ProfileResourceNode node) { - NodeMetaData metaData = node.getMetaData(); - addColumn(row, ID_ARRAY_INDEX, nullSafeNumber(node.getId())); - addColumn(row, PARENT_ID_ARRAY_INDEX, nullSafeNumber(node.getParentId())); - addColumn(row, URI_ARRAY_INDEX, DroidUrlFormat.format(node.getUri())); - addColumn(row, FILE_PATH_ARRAY_INDEX, toFilePath(node.getUri())); - addColumn(row, FILE_NAME_ARRAY_INDEX, toFileName(metaData.getName())); - addColumn(row, ID_METHOD_ARRAY_INDEX, nullSafeName(metaData.getIdentificationMethod())); - addColumn(row, STATUS_ARRAY_INDEX, metaData.getNodeStatus().getStatus()); - addColumn(row, SIZE_ARRAY_INDEX, nullSafeNumber(metaData.getSize())); - addColumn(row, RESOURCE_ARRAY_INDEX, metaData.getResourceType().getResourceType()); - addColumn(row, EXTENSION_ARRAY_INDEX, metaData.getExtension()); - addColumn(row, LAST_MODIFIED_ARRAY_INDEX, nullSafeDate(metaData.getLastModifiedDate(), dateFormat)); - addColumn(row, EXTENSION_MISMATCH_ARRAY_INDEX, node.getExtensionMismatch().toString()); - addColumn(row, HASH_ARRAY_INDEX, metaData.getHash()); - addColumn(row, ID_COUNT_ARRAY_INDEX, nullSafeNumber(node.getIdentificationCount())); - } - - private void addColumn(List row, int columnIndex, String value) { - if (columnsToWrite[columnIndex]) { - row.add(value); - } - } } diff --git a/droid-results/src/main/java/uk/gov/nationalarchives/droid/profile/CsvWriterConstants.java b/droid-results/src/main/java/uk/gov/nationalarchives/droid/profile/CsvWriterConstants.java new file mode 100644 index 000000000..3aa8ea43c --- /dev/null +++ b/droid-results/src/main/java/uk/gov/nationalarchives/droid/profile/CsvWriterConstants.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2016, The National Archives + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the The National Archives nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package uk.gov.nationalarchives.droid.profile; + +import org.apache.commons.lang.time.DateFormatUtils; +import org.apache.commons.lang.time.FastDateFormat; + +import java.util.Arrays; +import java.util.List; + +public final class CsvWriterConstants { + /** + * Default header for the corresponding column. + */ + public static final String HEADER_NAME_ID = "ID"; + /** + * Default header for the corresponding column. + */ + public static final String HEADER_NAME_PARENT_ID = "PARENT_ID"; + /** + * Default header for the corresponding column. + */ + public static final String HEADER_NAME_URI = "URI"; + /** + * Default header for the corresponding column. + */ + public static final String HEADER_NAME_FILE_PATH = "FILE_PATH"; + /** + * Default header for the corresponding column. + */ + public static final String HEADER_NAME_NAME = "NAME"; + /** + * Default header for the corresponding column. + */ + public static final String HEADER_NAME_METHOD = "METHOD"; + /** + * Default header for the corresponding column. + */ + public static final String HEADER_NAME_STATUS = "STATUS"; + /** + * Default header for the corresponding column. + */ + public static final String HEADER_NAME_SIZE = "SIZE"; + /** + * Default header for the corresponding column. + */ + public static final String HEADER_NAME_TYPE = "TYPE"; + /** + * Default header for the corresponding column. + */ + public static final String HEADER_NAME_EXT = "EXT"; + /** + * Default header for the corresponding column. + */ + public static final String HEADER_NAME_LAST_MODIFIED = "LAST_MODIFIED"; + /** + * Default header for the corresponding column. + */ + public static final String HEADER_NAME_EXTENSION_MISMATCH = "EXTENSION_MISMATCH"; + /** + * Default header for the corresponding column. + */ + public static final String HEADER_NAME_HASH = "HASH"; + /** + * Default header for the corresponding column. + */ + public static final String HEADER_NAME_FORMAT_COUNT = "FORMAT_COUNT"; + /** + * Default header for the corresponding column. + */ + public static final String HEADER_NAME_PUID = "PUID"; + /** + * Default header for the corresponding column. + */ + public static final String HEADER_NAME_MIME_TYPE = "MIME_TYPE"; + /** + * Default header for the corresponding column. + */ + public static final String HEADER_NAME_FORMAT_NAME = "FORMAT_NAME"; + /** + * Default header for the corresponding column. + */ + public static final String HEADER_NAME_FORMAT_VERSION = "FORMAT_VERSION"; + + /** + * Default dateTime format to be used for writing CSVs. + */ + public static final FastDateFormat DATE_FORMAT = DateFormatUtils.ISO_DATETIME_FORMAT; + + + /** + * Default index of the hash column, since this column header may be modified. + * This is the only column that we may need to access using index. + */ + public static final int HASH_ARRAY_INDEX = 12; + + /** + * String array for all the default headers. + */ + public static final String[] HEADERS = { + HEADER_NAME_ID, + HEADER_NAME_PARENT_ID, + HEADER_NAME_URI, + HEADER_NAME_FILE_PATH, + HEADER_NAME_NAME, + HEADER_NAME_METHOD, + HEADER_NAME_STATUS, + HEADER_NAME_SIZE, + HEADER_NAME_TYPE, + HEADER_NAME_EXT, + HEADER_NAME_LAST_MODIFIED, + HEADER_NAME_EXTENSION_MISMATCH, + HEADER_NAME_HASH, + HEADER_NAME_FORMAT_COUNT, + HEADER_NAME_PUID, + HEADER_NAME_MIME_TYPE, + HEADER_NAME_FORMAT_NAME, + HEADER_NAME_FORMAT_VERSION, + }; + + /** + * List of headers that appear more than once if a file matches more than one format. + */ + public static final List PER_FORMAT_HEADERS = Arrays.asList( + HEADER_NAME_PUID, HEADER_NAME_MIME_TYPE, HEADER_NAME_FORMAT_NAME, HEADER_NAME_FORMAT_VERSION); + + /** + * Empty string constant. + */ + public static final String EMPTY_STRING = ""; + + /** + * File uri scheme, used as prefix. + */ + public static final String FILE_URI_SCHEME = "file"; + + private CsvWriterConstants() { + //hidden constructor + } + +} diff --git a/droid-results/src/main/java/uk/gov/nationalarchives/droid/profile/datawriter/ColumnBasedDataWriter.java b/droid-results/src/main/java/uk/gov/nationalarchives/droid/profile/datawriter/ColumnBasedDataWriter.java new file mode 100644 index 000000000..6d7b38425 --- /dev/null +++ b/droid-results/src/main/java/uk/gov/nationalarchives/droid/profile/datawriter/ColumnBasedDataWriter.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2016, The National Archives + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the The National Archives nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package uk.gov.nationalarchives.droid.profile.datawriter; + +import com.univocity.parsers.csv.CsvWriter; +import uk.gov.nationalarchives.droid.core.interfaces.util.DroidUrlFormat; +import uk.gov.nationalarchives.droid.profile.CsvWriterConstants; +import uk.gov.nationalarchives.droid.profile.NodeMetaData; +import uk.gov.nationalarchives.droid.profile.ProfileResourceNode; +import uk.gov.nationalarchives.droid.profile.referencedata.Format; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * This class implements the methods for writing headers and data when the export is done using selection of various columns. + */ +public class ColumnBasedDataWriter extends FormattedDataWriter { + private final Map columnsToWriteMap; + + public ColumnBasedDataWriter(Map columnsToWriteMap) { + this.columnsToWriteMap = columnsToWriteMap; + } + + @Override + public void writeDataRowsForOneRowPerFile(List nodes, CsvWriter csvWriter) { + int maxIdCount = getMaxIdentificationCount(nodes); + for (ProfileResourceNode node : nodes) { + List nodeEntries = new ArrayList<>(); + addNodeColumnsInDefaultOrder(nodeEntries, node); + List formatIdentifications = node.getFormatIdentifications(); + for (int i = 0; i < maxIdCount; i++) { + addColumn(nodeEntries, CsvWriterConstants.HEADER_NAME_PUID, (i < formatIdentifications.size()) ? formatIdentifications.get(i).getPuid() : CsvWriterConstants.EMPTY_STRING); + addColumn(nodeEntries, CsvWriterConstants.HEADER_NAME_MIME_TYPE, (i < formatIdentifications.size()) ? formatIdentifications.get(i).getMimeType() : CsvWriterConstants.EMPTY_STRING); + addColumn(nodeEntries, CsvWriterConstants.HEADER_NAME_FORMAT_NAME, (i < formatIdentifications.size()) ? formatIdentifications.get(i).getName() : CsvWriterConstants.EMPTY_STRING); + addColumn(nodeEntries, CsvWriterConstants.HEADER_NAME_FORMAT_VERSION, (i < formatIdentifications.size()) ? formatIdentifications.get(i).getVersion(): CsvWriterConstants.EMPTY_STRING); + } + csvWriter.writeRow(nodeEntries); + } + csvWriter.flush(); + } + + @Override + public void writeDataRowsForOneRowPerFormat(List nodes, CsvWriter csvWriter) { + for (ProfileResourceNode node : nodes) { + for (Format format : node.getFormatIdentifications()) { + List nodeEntries = new ArrayList<>(); + addNodeColumnsInDefaultOrder(nodeEntries, node); + addColumn(nodeEntries, CsvWriterConstants.HEADER_NAME_PUID, format.getPuid()); + addColumn(nodeEntries, CsvWriterConstants.HEADER_NAME_MIME_TYPE, format.getMimeType()); + addColumn(nodeEntries, CsvWriterConstants.HEADER_NAME_FORMAT_NAME, format.getName()); + addColumn(nodeEntries, CsvWriterConstants.HEADER_NAME_FORMAT_VERSION, format.getVersion()); + csvWriter.writeRow(nodeEntries); + } + } + csvWriter.flush(); + } + + @Override + public void writeHeadersForOneRowPerFile(List nodes, String[] headers, CsvWriter csvWriter) { + super.setCustomisedHeaders(headers); + List headersToWrite = new ArrayList<>(getHeadersToWrite(getCustomisedHeaders())); + int maxIdCount = getMaxIdentificationCount(nodes); + + //if we are writing one row per file, then we tag the "per format" fields as additional columns, + //if such columns need to be added, we create appropriate headers with a running suffix and write + //them to the file + if (!Collections.disjoint(headersToWrite, CsvWriterConstants.PER_FORMAT_HEADERS) && (maxIdCount > 1)) { //add headers + for (int newColumnSuffix = 1; newColumnSuffix < maxIdCount; newColumnSuffix++) { + //"PUID","MIME_TYPE","FORMAT_NAME","FORMAT_VERSION" + for (String headerEntry : CsvWriterConstants.PER_FORMAT_HEADERS) { + if (headersToWrite.contains(headerEntry)) { + headersToWrite.add(headerEntry + newColumnSuffix); + } + } + } + } + + csvWriter.writeHeaders(headersToWrite); + csvWriter.flush(); + } + + @Override + public void writeHeadersForOneRowPerFormat(List nodes, String[] headers, CsvWriter csvWriter) { + super.setCustomisedHeaders(headers); + List headersToWrite = new ArrayList<>(getHeadersToWrite(getCustomisedHeaders())); + csvWriter.writeHeaders(headersToWrite); + csvWriter.flush(); + } + + private void addNodeColumnsInDefaultOrder(List row, ProfileResourceNode node) { + NodeMetaData metaData = node.getMetaData(); + addColumn(row, CsvWriterConstants.HEADER_NAME_ID, nullSafeNumber(node.getId())); + addColumn(row, CsvWriterConstants.HEADER_NAME_PARENT_ID, nullSafeNumber(node.getParentId())); + addColumn(row, CsvWriterConstants.HEADER_NAME_URI, DroidUrlFormat.format(node.getUri())); + addColumn(row, CsvWriterConstants.HEADER_NAME_FILE_PATH, toFilePath(node.getUri())); + addColumn(row, CsvWriterConstants.HEADER_NAME_NAME, toFileName(metaData.getName())); + addColumn(row, CsvWriterConstants.HEADER_NAME_METHOD, nullSafeName(metaData.getIdentificationMethod())); + addColumn(row, CsvWriterConstants.HEADER_NAME_STATUS, metaData.getNodeStatus().getStatus()); + addColumn(row, CsvWriterConstants.HEADER_NAME_SIZE, nullSafeNumber(metaData.getSize())); + addColumn(row, CsvWriterConstants.HEADER_NAME_TYPE, metaData.getResourceType().getResourceType()); + addColumn(row, CsvWriterConstants.HEADER_NAME_EXT, metaData.getExtension()); + addColumn(row, CsvWriterConstants.HEADER_NAME_LAST_MODIFIED, nullSafeDate(metaData.getLastModifiedDate(), CsvWriterConstants.DATE_FORMAT)); + addColumn(row, CsvWriterConstants.HEADER_NAME_EXTENSION_MISMATCH, node.getExtensionMismatch().toString()); + addColumn(row, CsvWriterConstants.HEADER_NAME_HASH, metaData.getHash()); + addColumn(row, CsvWriterConstants.HEADER_NAME_FORMAT_COUNT, nullSafeNumber(node.getIdentificationCount())); + } + + private void addColumn(List row, String columnName, String value) { + if (columnsToWriteMap.get(columnName)) { + row.add(value); + } + } + + private List getHeadersToWrite(String[] headersToWrite) { + if (columnsToWriteMap.containsValue(false)) { + int numColumnsToWrite = (int) columnsToWriteMap.values().stream().filter(eachVal -> eachVal).count(); + String[] newHeaders = new String[numColumnsToWrite]; + int newHeaderIndex = 0; + for (int i = 0; i < CsvWriterConstants.HEADERS.length; i++) { + if (columnsToWriteMap.get(CsvWriterConstants.HEADERS[i])) { + newHeaders[newHeaderIndex++] = getCustomisedHeaders()[i]; + } + } + return Arrays.stream(newHeaders).collect(Collectors.toList()); + } + return Arrays.stream(headersToWrite).collect(Collectors.toList()); + } + +} diff --git a/droid-results/src/main/java/uk/gov/nationalarchives/droid/profile/datawriter/DataWriterProvider.java b/droid-results/src/main/java/uk/gov/nationalarchives/droid/profile/datawriter/DataWriterProvider.java new file mode 100644 index 000000000..e760c588b --- /dev/null +++ b/droid-results/src/main/java/uk/gov/nationalarchives/droid/profile/datawriter/DataWriterProvider.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2016, The National Archives + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the The National Archives nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package uk.gov.nationalarchives.droid.profile.datawriter; + +import uk.gov.nationalarchives.droid.export.interfaces.ExportTemplate; + +import java.util.Map; + +/** + * Abstracts the creation of specific type of data writer away from the user. + */ +public final class DataWriterProvider { + + private DataWriterProvider() { + //hidden constructor + } + + /** + * Instantiae and return specific data writer (either column based, or template based). + * @param columnsToWriteMap Map with a list of columns to write + * @param exportTemplate exportTemplate + * @return Specific data writer. + */ + public static FormattedDataWriter getDataWriter(Map columnsToWriteMap, ExportTemplate exportTemplate) { + if (exportTemplate != null) { + return new TemplateBasedDataWriter(exportTemplate); + } else { + return new ColumnBasedDataWriter(columnsToWriteMap); + } + } +} diff --git a/droid-results/src/main/java/uk/gov/nationalarchives/droid/profile/datawriter/FormattedDataWriter.java b/droid-results/src/main/java/uk/gov/nationalarchives/droid/profile/datawriter/FormattedDataWriter.java new file mode 100644 index 000000000..ddb33c4fd --- /dev/null +++ b/droid-results/src/main/java/uk/gov/nationalarchives/droid/profile/datawriter/FormattedDataWriter.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2016, The National Archives + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the The National Archives nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package uk.gov.nationalarchives.droid.profile.datawriter; + +import com.univocity.parsers.csv.CsvWriter; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang.time.FastDateFormat; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.nationalarchives.droid.profile.CsvWriterConstants; +import uk.gov.nationalarchives.droid.profile.ProfileResourceNode; + +import java.io.File; +import java.net.URI; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Abstract class used fow writing headers and lines to the CSV writer. + * This class implements some common methods and other specific data writers are responsible for overriding + * the methods for writing headers and data + */ +public abstract class FormattedDataWriter { + private final Logger log = LoggerFactory.getLogger(getClass()); + + private String[] customisedHeaders; + + public abstract void writeHeadersForOneRowPerFile(List nodes, String[] headers, CsvWriter csvWriter); + public abstract void writeDataRowsForOneRowPerFile(List nodes, CsvWriter csvWriter); + public abstract void writeHeadersForOneRowPerFormat(List nodes, String[] headers, CsvWriter csvWriter); + public abstract void writeDataRowsForOneRowPerFormat(List nodes, CsvWriter csvWriter); + + protected static String nullSafeName(Enum value) { + return value == null ? CsvWriterConstants.EMPTY_STRING : value.toString(); + } + + protected static String nullSafeNumber(Number number) { + return number == null ? CsvWriterConstants.EMPTY_STRING : number.toString(); + } + + protected static String nullSafeDate(Date date, FastDateFormat format) { + return date == null ? CsvWriterConstants.EMPTY_STRING : format.format(date); + } + + protected static String toFileName(String name) { + return FilenameUtils.getName(name); + } + + protected String toFilePath(URI uri) { + if (uri == null) { + log.warn("[URI not set]"); + return CsvWriterConstants.EMPTY_STRING; + } + if (CsvWriterConstants.FILE_URI_SCHEME.equals(uri.getScheme())) { + return Paths.get(uri).toAbsolutePath().toString(); + } + + // for URIs that have other than "file" scheme + String result = java.net.URLDecoder.decode(uri.toString()).replaceAll("file://", CsvWriterConstants.EMPTY_STRING); + result = result.replace("/", File.separator); + + // Handle substitution of 7z + final String sevenZedIdentifier = "sevenz:"; + if (result.startsWith(sevenZedIdentifier)) { + result = "7z:" + result.substring(sevenZedIdentifier.length()); + } + + return result; + } + + protected int getMaxIdentificationCount(List nodes) { + Optional maxIdentificationsOption = nodes.stream().map(ProfileResourceNode::getIdentificationCount).collect(Collectors.toList()).stream().filter(Objects::nonNull).max(Integer::compare); + int maxIdentifications = 0; + if (maxIdentificationsOption.isPresent()) { + maxIdentifications = maxIdentificationsOption.get(); + } + return maxIdentifications; + } + + + protected void setCustomisedHeaders(final String[] customisedHeaders) { + this.customisedHeaders = Arrays.copyOf(customisedHeaders, customisedHeaders.length); + } + + protected String[] getCustomisedHeaders() { + return this.customisedHeaders; + } +} diff --git a/droid-results/src/main/java/uk/gov/nationalarchives/droid/profile/datawriter/TemplateBasedDataWriter.java b/droid-results/src/main/java/uk/gov/nationalarchives/droid/profile/datawriter/TemplateBasedDataWriter.java new file mode 100644 index 000000000..c3c9f3132 --- /dev/null +++ b/droid-results/src/main/java/uk/gov/nationalarchives/droid/profile/datawriter/TemplateBasedDataWriter.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2016, The National Archives + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the The National Archives nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package uk.gov.nationalarchives.droid.profile.datawriter; + +import com.univocity.parsers.csv.CsvWriter; +import uk.gov.nationalarchives.droid.core.interfaces.util.DroidUrlFormat; +import uk.gov.nationalarchives.droid.export.interfaces.ExportTemplate; +import uk.gov.nationalarchives.droid.export.interfaces.ExportTemplateColumnDef; +import uk.gov.nationalarchives.droid.profile.CsvWriterConstants; +import uk.gov.nationalarchives.droid.profile.NodeMetaData; +import uk.gov.nationalarchives.droid.profile.ProfileResourceNode; +import uk.gov.nationalarchives.droid.profile.referencedata.Format; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * This class implements the methods for writing headers and data when the export is done using an export template. + */ +public class TemplateBasedDataWriter extends FormattedDataWriter{ + private final ExportTemplate template; + public TemplateBasedDataWriter(ExportTemplate exportTemplate) { + this.template = exportTemplate; + } + + @Override + public void writeDataRowsForOneRowPerFile(List nodes, CsvWriter csvWriter) { + int maxIdCount = getMaxIdentificationCount(nodes); + Map columnPositions = template.getColumnOrderMap(); + int maxCols = columnPositions.keySet().stream().max(Integer::compare).get(); + for (ProfileResourceNode node : nodes) { + List nodeEntries = new ArrayList<>(); + for (int i = 0; i <= maxCols; i++) { + ExportTemplateColumnDef def = columnPositions.get(i); + if (def.getColumnType() == ExportTemplateColumnDef.ColumnType.ConstantString) { + nodeEntries.add(def.getDataValue()); + } else { + String columnName = def.getOriginalColumnName(); + if (CsvWriterConstants.PER_FORMAT_HEADERS.contains(columnName)) { + addFormatColumnTemplate(nodeEntries, node, def, maxIdCount); + } else { + addNodeColumn(nodeEntries, node, def); + } + } + } + csvWriter.writeRow(nodeEntries); + } + csvWriter.flush(); + } + + @Override + public void writeDataRowsForOneRowPerFormat(List nodes, CsvWriter csvWriter) { + Map columnPositions = template.getColumnOrderMap(); + int maxCols = columnPositions.keySet().stream().max(Integer::compare).get(); + for (ProfileResourceNode node : nodes) { + for (Format format : node.getFormatIdentifications()) { + List nodeEntries = new ArrayList<>(); + for (int i = 0; i <= maxCols; i++) { + ExportTemplateColumnDef def = columnPositions.get(i); + if (def.getColumnType() == ExportTemplateColumnDef.ColumnType.ConstantString) { + nodeEntries.add(def.getDataValue()); + } else { + String columnName = def.getOriginalColumnName(); + if (CsvWriterConstants.PER_FORMAT_HEADERS.contains(columnName)) { + String columnValue = getFormatValue(columnName, format); + nodeEntries.add(columnValue); + } else { + addNodeColumn(nodeEntries, node, def); + } + } + } + csvWriter.writeRow(nodeEntries); + } + } + csvWriter.flush(); + } + + @Override + public void writeHeadersForOneRowPerFile(List nodes, String[] headers, CsvWriter csvWriter) { + super.setCustomisedHeaders(headers); + int maxIdCount = getMaxIdentificationCount(nodes); + List headersToWrite = getHeadersToWrite(maxIdCount); + csvWriter.writeHeaders(headersToWrite); + csvWriter.flush(); + } + + @Override + public void writeHeadersForOneRowPerFormat(List nodes, String[] headers, CsvWriter csvWriter) { + super.setCustomisedHeaders(headers); + int maxIdColumns = 1; //for "per format" export, the additional identified formats are written as new row hence no additional id headers + List headersToWrite = getHeadersToWrite(maxIdColumns); + csvWriter.writeHeaders(headersToWrite); + csvWriter.flush(); + } + + private void addFormatColumnTemplate(List nodeEntries, ProfileResourceNode node, ExportTemplateColumnDef def, int maxIdCount) { + List formats = node.getFormatIdentifications(); + for (int i = 0; i < maxIdCount; i++) { + Format format = i < formats.size() ? formats.get(i) : null; + nodeEntries.add(def.getOperatedValue(getFormatValue(def.getOriginalColumnName(), format))); + } + } + + //CHECKSTYLE:OFF Swith without default + private String getFormatValue(String columnName, Format format) { + String retVal = CsvWriterConstants.EMPTY_STRING; + switch (columnName) { + case CsvWriterConstants.HEADER_NAME_PUID: + retVal = format == null ? CsvWriterConstants.EMPTY_STRING : format.getPuid(); + break; + case CsvWriterConstants.HEADER_NAME_MIME_TYPE: + retVal = format == null ? CsvWriterConstants.EMPTY_STRING : format.getMimeType(); + break; + case CsvWriterConstants.HEADER_NAME_FORMAT_NAME: + retVal = format == null ? CsvWriterConstants.EMPTY_STRING : format.getName(); + break; + case CsvWriterConstants.HEADER_NAME_FORMAT_VERSION: + retVal = format == null ? CsvWriterConstants.EMPTY_STRING : format.getVersion(); + } + return retVal; + } + //CHECKSTYLE:ON + + //CHECKSTYLE:OFF - cyclomatic complexity is too high but we have to go through so many column names!! + private void addNodeColumn(List nodeEntries, ProfileResourceNode node, ExportTemplateColumnDef def) { + NodeMetaData metaData = node.getMetaData(); + + String columnValue = ""; + switch (def.getOriginalColumnName()) { + case CsvWriterConstants.HEADER_NAME_ID: + columnValue = nullSafeNumber(node.getId()); + break; + case CsvWriterConstants.HEADER_NAME_PARENT_ID: + columnValue = nullSafeNumber(node.getParentId()); + break; + case CsvWriterConstants.HEADER_NAME_URI: + columnValue = DroidUrlFormat.format(node.getUri()); + break; + case CsvWriterConstants.HEADER_NAME_FILE_PATH: + columnValue = toFilePath(node.getUri()); + break; + case CsvWriterConstants.HEADER_NAME_NAME: + columnValue = toFileName(metaData.getName()); + break; + case CsvWriterConstants.HEADER_NAME_METHOD: + columnValue = nullSafeName(metaData.getIdentificationMethod()); + break; + case CsvWriterConstants.HEADER_NAME_STATUS: + columnValue = metaData.getNodeStatus().getStatus(); + break; + case CsvWriterConstants.HEADER_NAME_SIZE: + columnValue = nullSafeNumber(metaData.getSize()); + break; + case CsvWriterConstants.HEADER_NAME_TYPE: + columnValue = metaData.getResourceType().getResourceType(); + break; + case CsvWriterConstants.HEADER_NAME_EXT: + columnValue = metaData.getExtension(); + break; + case CsvWriterConstants.HEADER_NAME_LAST_MODIFIED: + columnValue = nullSafeDate(metaData.getLastModifiedDate(), CsvWriterConstants.DATE_FORMAT); + break; + case CsvWriterConstants.HEADER_NAME_EXTENSION_MISMATCH: + columnValue = node.getExtensionMismatch().toString(); + break; + case CsvWriterConstants.HEADER_NAME_HASH: + columnValue = metaData.getHash(); + break; + case CsvWriterConstants.HEADER_NAME_FORMAT_COUNT: + columnValue = nullSafeNumber(node.getIdentificationCount()); + break; + } + nodeEntries.add(def.getOperatedValue(columnValue)); + } + //CHECKSTYLE:ON + + private List getHeadersToWrite(int maxIdCount) { + if (template == null) { + throw new IllegalArgumentException("Export template does not exist, unable to get headers from template"); + } + List retVal = new LinkedList<>(); + + Map columnOrderMap = template.getColumnOrderMap(); + for (int i = 0; i < columnOrderMap.size(); i++) { + ExportTemplateColumnDef def = columnOrderMap.get(i); + retVal.add(def.getHeaderLabel()); + if (def.getColumnType() != ExportTemplateColumnDef.ColumnType.ConstantString) { + if (CsvWriterConstants.PER_FORMAT_HEADERS.contains(def.getOriginalColumnName())) { + for (int newColumnSuffix = 1; newColumnSuffix < maxIdCount; newColumnSuffix++) { + retVal.add(def.getHeaderLabel() + newColumnSuffix); + } + } + } + } + return retVal; + } +} diff --git a/droid-results/src/test/java/uk/gov/nationalarchives/droid/profile/CsvItemWriterTest.java b/droid-results/src/test/java/uk/gov/nationalarchives/droid/profile/CsvItemWriterTest.java index b855248c1..5eec88c5b 100644 --- a/droid-results/src/test/java/uk/gov/nationalarchives/droid/profile/CsvItemWriterTest.java +++ b/droid-results/src/test/java/uk/gov/nationalarchives/droid/profile/CsvItemWriterTest.java @@ -45,6 +45,8 @@ import uk.gov.nationalarchives.droid.core.interfaces.config.DroidGlobalConfig; import uk.gov.nationalarchives.droid.core.interfaces.config.DroidGlobalProperty; import uk.gov.nationalarchives.droid.export.interfaces.ExportOptions; +import uk.gov.nationalarchives.droid.export.interfaces.ExportTemplate; +import uk.gov.nationalarchives.droid.export.interfaces.ExportTemplateColumnDef; import uk.gov.nationalarchives.droid.profile.referencedata.Format; import java.io.File; @@ -54,9 +56,12 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -80,7 +85,12 @@ public class CsvItemWriterTest { private CsvItemWriter itemWriter; private DroidGlobalConfig config; private String testDateTimeString; - + + private static final String defaultHeaders = toCsvRow(new String[] { + "ID","PARENT_ID","URI","FILE_PATH","NAME","METHOD","STATUS","SIZE","TYPE","EXT","LAST_MODIFIED","EXTENSION_MISMATCH","HASH","FORMAT_COUNT", + "PUID","MIME_TYPE","FORMAT_NAME","FORMAT_VERSION" + }); + @Before public void setup() { File dir = new File("exports"); @@ -105,7 +115,7 @@ private static String toCsvRow(final String[] values) { } @Test - public void testWriteNoNodes() throws IOException { + public void should_write_only_headers_when_there_are_no_nodes_to_be_written() throws IOException { when(config.getBooleanProperty(DroidGlobalProperty.CSV_EXPORT_ROW_PER_FORMAT)).thenReturn(false); try(final Writer writer = new StringWriter()) { @@ -113,12 +123,14 @@ public void testWriteNoNodes() throws IOException { itemWriter.open(writer); itemWriter.write(nodes); - assertEquals(1, writer.toString().split("\n").length); + String[] writtenLines = writer.toString().split(LINE_SEPARATOR); + assertEquals(1, writtenLines.length); + assertEquals(defaultHeaders, writtenLines[0]); } } @Test - public void testWriteOneNode() throws IOException { + public void should_write_one_node_as_one_row_per_format_export() throws IOException { when(config.getBooleanProperty(DroidGlobalProperty.CSV_EXPORT_ROW_PER_FORMAT)).thenReturn(false); try(final Writer writer = new StringWriter()) { @@ -153,6 +165,7 @@ public void testWriteOneNode() throws IOException { final String[] lines = writer.toString().split(LINE_SEPARATOR); assertEquals(2, lines.length); + assertEquals(defaultHeaders, lines[0]); assertEquals(expectedEntry, lines[1]); } } @@ -299,6 +312,274 @@ public void shouldCreateAdditionalHeadersForIdentificationFormatsWhenWritingAnEn } } + @Test + public void should_create_additional_headers_for_identification_format_in_order_of_template_when_writing_entries_per_file() throws IOException { + when(config.getBooleanProperty(DroidGlobalProperty.CSV_EXPORT_ROW_PER_FORMAT)).thenReturn(false); + ExportTemplate template = mock(ExportTemplate.class); + + Map columnPositions = new HashMap<>(); + + ExportTemplateColumnDef def1 = getMockColumnDef("ID", "Identifier", ExportTemplateColumnDef.ColumnType.ProfileResourceNode); + ExportTemplateColumnDef def2 = getMockColumnDef("PUID", "Puid", ExportTemplateColumnDef.ColumnType.ProfileResourceNode); + ExportTemplateColumnDef def3 = getMockColumnDef("HASH", "Hash123", ExportTemplateColumnDef.ColumnType.ProfileResourceNode); + ExportTemplateColumnDef def4 = getMockColumnDef("FORMAT_NAME", "Format_Name", ExportTemplateColumnDef.ColumnType.ProfileResourceNode); + ExportTemplateColumnDef def5 = getMockColumnDef("Simple Column", "Simple_Header", ExportTemplateColumnDef.ColumnType.ConstantString); + + columnPositions.put(0, def1); + columnPositions.put(1, def2); + columnPositions.put(2, def3); + columnPositions.put(3, def4); + columnPositions.put(4, def5); + + when(template.getColumnOrderMap()).thenReturn(columnPositions); + + try(final Writer writer = new StringWriter()) { + List nodes = new ArrayList<>(); + + Format id1 = buildFormat(1); + Format id2 = buildFormat(2); + + ProfileResourceNode node = buildProfileResourceNode(1, 1000L); + node.addFormatIdentification(id1); + node.addFormatIdentification(id2); + nodes.add(node); + + itemWriter.setOptions(ExportOptions.ONE_ROW_PER_FILE); + itemWriter.setExportTemplate(template); + itemWriter.open(writer); + itemWriter.write(nodes); + + final String expectedHeaders = toCsvRow(new String[] { + "Identifier", "Puid", "Puid1", "Hash123", "Format_Name", "Format_Name1", "Simple_Header" + }); + + final String[] lines = writer.toString().split(LINE_SEPARATOR); + + assertEquals(2, lines.length); + assertEquals(expectedHeaders, lines[0]); + assertEquals(7, lines[1].split(",").length); + } + } + + @Test + public void should_write_a_column_with_pre_defined_constant_value() throws IOException { + when(config.getBooleanProperty(DroidGlobalProperty.CSV_EXPORT_ROW_PER_FORMAT)).thenReturn(false); + ExportTemplate template = mock(ExportTemplate.class); + + Map columnPositions = new HashMap<>(); + + ExportTemplateColumnDef def1 = getMockColumnDef("ID", "Identifier", ExportTemplateColumnDef.ColumnType.ProfileResourceNode); + ExportTemplateColumnDef def2 = getMockColumnDef("Simplified English", "Language", ExportTemplateColumnDef.ColumnType.ConstantString); + ExportTemplateColumnDef def3 = getMockColumnDef("FORMAT_NAME", "Format_Name", ExportTemplateColumnDef.ColumnType.ProfileResourceNode); + + columnPositions.put(0, def1); + columnPositions.put(1, def2); + columnPositions.put(2, def3); + + when(template.getColumnOrderMap()).thenReturn(columnPositions); + + try(final Writer writer = new StringWriter()) { + List nodes = new ArrayList<>(); + + Format id1 = buildFormat(1); + + ProfileResourceNode node = buildProfileResourceNode(1, 1000L); + node.addFormatIdentification(id1); + nodes.add(node); + + itemWriter.setOptions(ExportOptions.ONE_ROW_PER_FILE); + itemWriter.setExportTemplate(template); + itemWriter.open(writer); + itemWriter.write(nodes); + + final String expectedHeaders = toCsvRow(new String[] { + "Identifier", "Language", "Format_Name" + }); + + final String expectedEntry = toCsvRow(new String[] { + "", + "Simplified English", + "Plain Text" + }); + + + final String[] lines = writer.toString().split(LINE_SEPARATOR); + + assertEquals(2, lines.length); + assertEquals(expectedHeaders, lines[0]); + assertEquals(expectedEntry, lines[1]); + } + + } + + @Test + public void should_write_a_column_with_modified_value_and_a_pre_defined_constant_value() throws IOException { + when(config.getBooleanProperty(DroidGlobalProperty.CSV_EXPORT_ROW_PER_FORMAT)).thenReturn(false); + ExportTemplate template = mock(ExportTemplate.class); + + Map columnPositions = new HashMap<>(); + + ExportTemplateColumnDef def1 = getMockColumnDef("ID", "Identifier", ExportTemplateColumnDef.ColumnType.ProfileResourceNode); + ExportTemplateColumnDef def2 = getMockColumnDef("Simplified English", "Language", ExportTemplateColumnDef.ColumnType.ConstantString); + ExportTemplateColumnDef def3 = getMockColumnDef("FORMAT_NAME", "Format_Name", ExportTemplateColumnDef.ColumnType.DataModifier); + + + columnPositions.put(0, def1); + columnPositions.put(1, def2); + columnPositions.put(2, def3); + + when(template.getColumnOrderMap()).thenReturn(columnPositions); + + try(final Writer writer = new StringWriter()) { + List nodes = new ArrayList<>(); + + Format id1 = buildFormat(1); + + ProfileResourceNode node = buildProfileResourceNode(1, 1000L); + node.addFormatIdentification(id1); + nodes.add(node); + + itemWriter.setOptions(ExportOptions.ONE_ROW_PER_FILE); + itemWriter.setExportTemplate(template); + itemWriter.open(writer); + itemWriter.write(nodes); + + final String expectedHeaders = toCsvRow(new String[] { + "Identifier", "Language", "Format_Name" + }); + + final String expectedEntry = toCsvRow(new String[] { + "", + "Simplified English", + "plain text" + }); + + + final String[] lines = writer.toString().split(LINE_SEPARATOR); + + assertEquals(2, lines.length); + assertEquals(expectedHeaders, lines[0]); + assertEquals(expectedEntry, lines[1]); + } + + } + + @Test + public void should_restrict_to_limited_number_of_columns_when_the_column_names_are_set_on_the_writer_writing_per_file() throws IOException { + + try(final Writer writer = new StringWriter()) { + List nodes = new ArrayList<>(); + + Format id1 = buildFormat(1); + Format id2 = buildFormat(2); + + ProfileResourceNode node = buildProfileResourceNode(1, 1000L); + node.addFormatIdentification(id1); + node.addFormatIdentification(id2); + + nodes.add(node); + itemWriter.setOptions(ExportOptions.ONE_ROW_PER_FILE); + itemWriter.setColumnsToWrite("ID FORMAT_NAME"); + itemWriter.open(writer); + itemWriter.write(nodes); + + final String expectedHeaders = toCsvRow(new String[] { + "ID", "FORMAT_NAME", "FORMAT_NAME1" //per format columns + }); + + final String expectedEntry1 = toCsvRow(new String[] { + "", + "Plain Text", + "Plain Text" + }); + + final String[] lines = writer.toString().split(LINE_SEPARATOR); + + assertEquals(2, lines.length); + assertEquals(expectedHeaders, lines[0]); + assertEquals(expectedEntry1, lines[1]); + } + } + + @Test + public void should_restrict_to_limited_number_of_columns_when_the_column_names_are_set_on_the_writer_writing_per_format() throws IOException { + + try(final Writer writer = new StringWriter()) { + List nodes = new ArrayList<>(); + + Format id1 = buildFormat(1); + Format id2 = buildFormat(2); + + ProfileResourceNode node = buildProfileResourceNode(1, 1000L); + node.addFormatIdentification(id1); + node.addFormatIdentification(id2); + + nodes.add(node); + itemWriter.setOptions(ExportOptions.ONE_ROW_PER_FORMAT); + itemWriter.setColumnsToWrite("ID NAME FORMAT_NAME FORMAT_VERSION"); + itemWriter.open(writer); + itemWriter.write(nodes); + + final String expectedHeaders = toCsvRow(new String[] { + "ID", "NAME", "FORMAT_NAME", "FORMAT_VERSION" //per format columns + }); + + final String expectedEntry1 = toCsvRow(new String[] { + "", + "file1.txt", + "Plain Text", + "1.0" + }); + + final String[] lines = writer.toString().split(LINE_SEPARATOR); + + assertEquals(3, lines.length); + assertEquals(expectedHeaders, lines[0]); + assertEquals(expectedEntry1, lines[1]); + assertEquals(expectedEntry1, lines[2]); + } + } + + @Test + public void should_do_header_customisations_for_hash_column_when_an_algorithm_is_present() throws IOException { + try(final Writer writer = new StringWriter()) { + List nodes = new ArrayList<>(); + + Format id1 = buildFormat(1); + + ProfileResourceNode node = buildProfileResourceNode(1, 1000L); + node.addFormatIdentification(id1); + + nodes.add(node); + itemWriter.setOptions(ExportOptions.ONE_ROW_PER_FORMAT); + + Map headerCustomisation = new HashMap<>(); + headerCustomisation.put("hash", "XTRA_STRONG_HASH"); + itemWriter.setHeaders(headerCustomisation); + itemWriter.setColumnsToWrite("ID NAME FORMAT_NAME HASH"); + itemWriter.open(writer); + itemWriter.write(nodes); + + final String expectedHeaders = toCsvRow(new String[] { + "ID", "NAME", "XTRA_STRONG_HASH", "FORMAT_NAME" + }); + + final String expectedEntry1 = toCsvRow(new String[] { + "", + "file1.txt", + "11111111111111111111111111111111", + "Plain Text" + }); + + final String[] lines = writer.toString().split(LINE_SEPARATOR); + + assertEquals(2, lines.length); + assertEquals(expectedHeaders, lines[0]); + assertEquals(expectedEntry1, lines[1]); + } + + } + @Test public void testWriteOneNodeWithTwoFormatsWithOneRowPerFormat() throws IOException { when(config.getBooleanProperty(DroidGlobalProperty.CSV_EXPORT_ROW_PER_FORMAT)).thenReturn(true); @@ -307,7 +588,7 @@ public void testWriteOneNodeWithTwoFormatsWithOneRowPerFormat() throws IOExcepti List nodes = new ArrayList<>(); Format id1 = buildFormat(1); - Format id2 = buildFormat(2); + Format id2 = buildFormat(2, "2.0"); ProfileResourceNode node = buildProfileResourceNode(1, 1000L); node.addFormatIdentification(id1); @@ -355,7 +636,7 @@ public void testWriteOneNodeWithTwoFormatsWithOneRowPerFormat() throws IOExcepti "fmt/2", "text/plain", "Plain Text", - "1.0", + "2.0", }); final String[] lines = writer.toString().split(LINE_SEPARATOR); @@ -422,6 +703,88 @@ public void should_write_nodes_inside_archives_using_seaparators_depending_on_OS } } + @Test + public void should_write_data_row_with_additional_blank_elements_when_other_rows_have_more_identified_formats() throws IOException { + when(config.getBooleanProperty(DroidGlobalProperty.CSV_EXPORT_ROW_PER_FORMAT)).thenReturn(false); + + try(final Writer writer = new StringWriter()) { + List nodes = new ArrayList<>(); + + Format id1 = buildFormat(1); + Format id2 = buildFormat(2); + + ProfileResourceNode nodeWithTwoIdentifications = buildProfileResourceNode(1, 1000L); + nodeWithTwoIdentifications.addFormatIdentification(id1); + nodeWithTwoIdentifications.addFormatIdentification(id2); + + ProfileResourceNode nodeWithOneIentifications = buildProfileResourceNode(2, 500L); + Format id3 = buildFormat(3); + nodeWithOneIentifications.addFormatIdentification(id3); + + nodes.add(nodeWithTwoIdentifications); + nodes.add(nodeWithOneIentifications); + + itemWriter.setOptions(ExportOptions.ONE_ROW_PER_FILE); + itemWriter.setColumnsToWrite("ID PUID FORMAT_NAME METHOD"); + itemWriter.open(writer); + itemWriter.write(nodes); + + final String expectedHeaders = toCsvRow(new String[] { + "ID","METHOD","PUID","FORMAT_NAME","PUID1","FORMAT_NAME1" + }); + + final String expectedEntry1 = toCsvRow(new String[] { + "", + "Signature", + "fmt/1", + "Plain Text", + "fmt/2", + "Plain Text" + }); + + final String expectedEntry2 = toCsvRow(new String[] { + "", + "Signature", + "fmt/3", + "Plain Text", + "", + "" + }); + + final String[] lines = writer.toString().split(LINE_SEPARATOR); + + assertEquals(3, lines.length); + assertEquals(expectedHeaders, lines[0]); + assertEquals(expectedEntry1, lines[1]); + assertEquals(expectedEntry2, lines[2]); + } + } + + private static ExportTemplateColumnDef getMockColumnDef(String param1, String header, ExportTemplateColumnDef.ColumnType columnType) { + ExportTemplateColumnDef def = mock(ExportTemplateColumnDef.class); + when(def.getColumnType()).thenReturn(columnType); + switch (columnType) { + case ProfileResourceNode: + when(def.getOriginalColumnName()).thenReturn(param1); + when(def.getHeaderLabel()).thenReturn(header); + when(def.getDataValue()).thenThrow(new RuntimeException("Profile resource node column uses data from the profile results")); + when(def.getOperatedValue(anyString())).thenAnswer(i -> i.getArguments()[0]); + break; + case ConstantString: + when(def.getOriginalColumnName()).thenThrow(new RuntimeException("Constant String Columns do not have an associated original column name")); + when(def.getHeaderLabel()).thenReturn(header); + when(def.getDataValue()).thenReturn(param1); + break; + case DataModifier: + when(def.getOriginalColumnName()).thenReturn(param1); + when(def.getHeaderLabel()).thenReturn(header); + when(def.getDataValue()).thenThrow(new RuntimeException("Profile resource node column uses data from the profile results")); + when(def.getOperatedValue(anyString())).thenAnswer(i -> i.getArguments()[0].toString().toLowerCase()); + break; + } + return def; + } + private static boolean isNotWindows() { return !SystemUtils.IS_OS_WINDOWS; } @@ -451,12 +814,17 @@ private static ProfileResourceNode buildProfileResourceNode(int i, Long size, UR private static Format buildFormat(int i) { + return buildFormat(i, "1.0"); + } + + private static Format buildFormat(int i, String version) { Format format = new Format(); format.setPuid("fmt/" + i); format.setMimeType("text/plain"); format.setName("Plain Text"); - format.setVersion("1.0"); - + format.setVersion(version); + return format; } + } diff --git a/droid-swing-ui/src/main/java/uk/gov/nationalarchives/droid/gui/DroidMainFrame.java b/droid-swing-ui/src/main/java/uk/gov/nationalarchives/droid/gui/DroidMainFrame.java index 15ef8e7c0..cd8e15b9f 100644 --- a/droid-swing-ui/src/main/java/uk/gov/nationalarchives/droid/gui/DroidMainFrame.java +++ b/droid-swing-ui/src/main/java/uk/gov/nationalarchives/droid/gui/DroidMainFrame.java @@ -1460,6 +1460,9 @@ private void export() { } else { exportOptions.setExportOptions(ExportOptions.ONE_ROW_PER_FILE); } + + exportOptions.setDefaultTemplatesFolder(globalContext.getGlobalConfig().getExportTemplatesDir()); + exportOptions.showDialog(); if (exportOptions.isApproved()) { String columnNames = exportOptions.getColumnsToExport(); @@ -1486,6 +1489,7 @@ private void export() { exportAction.setBom(exportOptions.isBom()); exportAction.setQuoteAllFields(exportOptions.getQuoteAllColumns()); exportAction.setColumnsToWrite(columnNames); + exportAction.setExportTemplatePath(exportOptions.getTemplatePath()); exportAction.setCallback(new ActionDoneCallback() { @Override diff --git a/droid-swing-ui/src/main/java/uk/gov/nationalarchives/droid/gui/export/ExportAction.java b/droid-swing-ui/src/main/java/uk/gov/nationalarchives/droid/gui/export/ExportAction.java index f8684471e..5387362bf 100644 --- a/droid-swing-ui/src/main/java/uk/gov/nationalarchives/droid/gui/export/ExportAction.java +++ b/droid-swing-ui/src/main/java/uk/gov/nationalarchives/droid/gui/export/ExportAction.java @@ -42,6 +42,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import uk.gov.nationalarchives.droid.export.interfaces.ExportDetails; import uk.gov.nationalarchives.droid.export.interfaces.ExportManager; import uk.gov.nationalarchives.droid.export.interfaces.ExportOptions; import uk.gov.nationalarchives.droid.gui.action.ActionDoneCallback; @@ -61,6 +62,7 @@ public class ExportAction extends SwingWorker { private boolean bom; private boolean quoteAllFields; private String columnsToWrite; + private String exportTemplatePath; private List profileIds; private ActionDoneCallback callback; @@ -122,8 +124,7 @@ protected void process(List chunks) { @Override protected Void doInBackground() { //TODO: configure columns to write in UI. - exportTask = exportManager.exportProfiles(profileIds, destination.getPath(), null, options, - outputEncoding, bom, quoteAllFields, columnsToWrite); + exportTask = exportManager.exportProfiles(profileIds, destination.getPath(), null, getExportDetails()); try { exportTask.get(); } catch (InterruptedException e) { @@ -213,4 +214,28 @@ public void setQuoteAllFields(boolean quoteAllFields) { public void setColumnsToWrite(String columnsToWrite) { this.columnsToWrite = columnsToWrite; } + + /** + * Set the absolute path of the export template to the ExportAction. + * @param exportTemplatePath absolute path of the export template + */ + public void setExportTemplatePath(String exportTemplatePath) { + this.exportTemplatePath = exportTemplatePath; + } + + /** + * + * @return the export details for this export command. + */ + private ExportDetails getExportDetails() { + ExportDetails.ExportDetailsBuilder builder = new ExportDetails.ExportDetailsBuilder(); + return builder.withExportOptions(options) + .withOutputEncoding(outputEncoding) + .withBomFlag(bom) + .withQuotingAllFields(quoteAllFields) + .withColumnsToWrite(columnsToWrite) + .withExportTemplatePath(exportTemplatePath) + .build(); + } + } diff --git a/droid-swing-ui/src/main/java/uk/gov/nationalarchives/droid/gui/export/ExportDialog.form b/droid-swing-ui/src/main/java/uk/gov/nationalarchives/droid/gui/export/ExportDialog.form index 5c377af0c..4c8fd6d28 100644 --- a/droid-swing-ui/src/main/java/uk/gov/nationalarchives/droid/gui/export/ExportDialog.form +++ b/droid-swing-ui/src/main/java/uk/gov/nationalarchives/droid/gui/export/ExportDialog.form @@ -10,7 +10,13 @@ + + + + + + @@ -30,33 +36,33 @@ - + - + - + - - - + + + - - + + - - + + - + - - + + @@ -69,7 +75,7 @@ - + @@ -96,7 +102,14 @@ - + + + + + + + + @@ -104,30 +117,34 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - @@ -267,269 +284,396 @@ - + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + - - - - - - - - - - - + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + diff --git a/droid-swing-ui/src/main/java/uk/gov/nationalarchives/droid/gui/export/ExportDialog.java b/droid-swing-ui/src/main/java/uk/gov/nationalarchives/droid/gui/export/ExportDialog.java index e2677afca..139773350 100644 --- a/droid-swing-ui/src/main/java/uk/gov/nationalarchives/droid/gui/export/ExportDialog.java +++ b/droid-swing-ui/src/main/java/uk/gov/nationalarchives/droid/gui/export/ExportDialog.java @@ -31,14 +31,23 @@ */ package uk.gov.nationalarchives.droid.gui.export; +import java.awt.BorderLayout; +import java.awt.CardLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.io.IOException; import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; +import java.util.LinkedList; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.swing.BorderFactory; import javax.swing.ButtonGroup; import javax.swing.ComboBoxModel; @@ -57,6 +66,8 @@ import javax.swing.GroupLayout.Alignment; import javax.swing.JComboBox; import javax.swing.LayoutStyle.ComponentPlacement; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableCellRenderer; @@ -65,6 +76,7 @@ import uk.gov.nationalarchives.droid.export.interfaces.ExportOptions; import uk.gov.nationalarchives.droid.gui.DroidMainFrame; import uk.gov.nationalarchives.droid.gui.ProfileForm; +import uk.gov.nationalarchives.droid.gui.util.SortedComboBoxModel; /** * @@ -74,12 +86,14 @@ public class ExportDialog extends JDialog { private static final long serialVersionUID = -4598078880004073202L; private static final int CAPACITY = 128; + private static final String EXPORT_TEMPLATE_FILE_EXTENSION = ".template"; - private DroidMainFrame droidMain; + private final DroidMainFrame droidMain; private DefaultTableModel tableModel; private List profilesRowData; private boolean approved; - + private Path exportTemplatesFolder; + /** * Creates new form ReportDialog. * @param parent the dialog's parent @@ -90,7 +104,7 @@ public ExportDialog(final DroidMainFrame parent) { setModal(true); initComponents(); - jScrollPane1.getViewport().setBackground(profileSelectTable.getBackground()); + jScrollPaneProfileSelection.getViewport().setBackground(profileSelectTable.getBackground()); pack(); setLocationRelativeTo(getParent()); @@ -129,9 +143,17 @@ public boolean isCellEditable(int row, int column) { profileSelectTable.setDefaultEditor(ProfileForm.class, new CheckBoxEditor()); profileSelectTable.setDefaultRenderer(ProfileForm.class, new CheckBoxRenderer()); - jScrollPane1.setColumnHeaderView(null); + jScrollPaneProfileSelection.setColumnHeaderView(null); profileSelectTable.setCellSelectionEnabled(false); - + + ComboBoxModel templatesModel = getExportTemplatesModel(); + if (templatesModel.getSize() == 0) { + jCheckBoxUseTemplate.setEnabled(false); + } else { + jComboBox1.setModel(templatesModel); + jComboBox1.setSelectedItem(templatesModel.getElementAt(0)); + } + enableGenerateButton(); approved = false; setVisible(true); @@ -184,7 +206,7 @@ public void setExportOptions(ExportOptions options) { * @return the profilesRowData */ public List getSelectedProfileIds() { - List selectedProfiles = new ArrayList(); + List selectedProfiles = new ArrayList<>(); for (ProfileWrapper profileWrapper : profilesRowData) { if (profileWrapper.isSelected()) { @@ -228,6 +250,19 @@ public String getColumnsToExport() { return builder.toString().trim(); } + public String getTemplatePath() { + if (jCheckBoxUseTemplate.isSelected()) { + ExportTemplateComboBoxItem item = (ExportTemplateComboBoxItem) jComboBox1.getSelectedItem(); + return item.getTemplatePath().toAbsolutePath().toString(); + } else { + return null; + } + } + + public void setDefaultTemplatesFolder(Path templatesFolder) { + this.exportTemplatesFolder = templatesFolder; + } + private void addColumn(String columnName, boolean selected, StringBuilder builder) { if (selected) { builder.append(columnName).append(' '); @@ -251,9 +286,9 @@ private void initComponents() { buttonGroup1 = new ButtonGroup(); profileSelectLabel = new JLabel(); - jScrollPane1 = new JScrollPane(); + jScrollPaneProfileSelection = new JScrollPane(); profileSelectTable = new JTable(); - jPanel1 = new JPanel(); + jPanelBottomControl = new JPanel(); cancelButton = new JButton(); exportButton = new JButton(); RadioOneRowPerFile = new JRadioButton(); @@ -263,7 +298,9 @@ private void initComponents() { jCheckBoxQuoteAll = new JCheckBox(); toggleColumnButton = new JButton(); jButtonSetAllColumns = new JButton(); - jPanel2 = new JPanel(); + jPanelRight = new JPanel(); + jPanelCards = new JPanel(); + jPanelColumnChoices = new JPanel(); profileSelectLabel1 = new JLabel(); jCheckBoxId = new JCheckBox(); jCheckBoxParentId = new JCheckBox(); @@ -283,14 +320,20 @@ private void initComponents() { jCheckBoxFormatVersion = new JCheckBox(); jCheckBoxExtMismatch = new JCheckBox(); jCheckBoxFileHash = new JCheckBox(); + jPanelTemplateChoices = new JPanel(); + jLabel2 = new JLabel(); + jComboBox1 = new JComboBox<>(); + jCheckBoxUseTemplate = new JCheckBox(); setTitle(NbBundle.getMessage(ExportDialog.class, "ExportDialog.title_1")); // NOI18N setAlwaysOnTop(true); + setMinimumSize(new Dimension(1155, 835)); setName("exportDialog"); // NOI18N + setPreferredSize(new Dimension(1055, 760)); profileSelectLabel.setText(NbBundle.getMessage(ExportDialog.class, "ExportDialog.profileSelectLabel.text_1")); // NOI18N - jScrollPane1.setPreferredSize(new Dimension(300, 402)); + jScrollPaneProfileSelection.setPreferredSize(new Dimension(300, 402)); profileSelectTable.setModel(new DefaultTableModel( new Object [][] { @@ -302,7 +345,9 @@ private void initComponents() { )); profileSelectTable.setRowSelectionAllowed(false); profileSelectTable.setTableHeader(null); - jScrollPane1.setViewportView(profileSelectTable); + jScrollPaneProfileSelection.setViewportView(profileSelectTable); + + jPanelBottomControl.setBorder(BorderFactory.createEmptyBorder(1, 1, 10, 1)); cancelButton.setText(NbBundle.getMessage(ExportDialog.class, "ExportDialog.cancelButton.text")); // NOI18N cancelButton.setVerticalAlignment(SwingConstants.BOTTOM); @@ -357,45 +402,47 @@ public void actionPerformed(ActionEvent evt) { } }); - GroupLayout jPanel1Layout = new GroupLayout(jPanel1); - jPanel1.setLayout(jPanel1Layout); - jPanel1Layout.setHorizontalGroup(jPanel1Layout.createParallelGroup(Alignment.LEADING) - .addGroup(jPanel1Layout.createSequentialGroup() + GroupLayout jPanelBottomControlLayout = new GroupLayout(jPanelBottomControl); + jPanelBottomControl.setLayout(jPanelBottomControlLayout); + jPanelBottomControlLayout.setHorizontalGroup(jPanelBottomControlLayout.createParallelGroup(Alignment.LEADING) + .addGroup(jPanelBottomControlLayout.createSequentialGroup() .addContainerGap() - .addGroup(jPanel1Layout.createParallelGroup(Alignment.LEADING) - .addGroup(Alignment.TRAILING, jPanel1Layout.createSequentialGroup() - .addComponent(jLabel1) - .addGap(18, 18, 18) - .addComponent(cmdEncoding, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) - .addPreferredGap(ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(exportButton) - .addPreferredGap(ComponentPlacement.UNRELATED) - .addComponent(cancelButton, GroupLayout.PREFERRED_SIZE, 125, GroupLayout.PREFERRED_SIZE)) - .addGroup(jPanel1Layout.createSequentialGroup() + .addGroup(jPanelBottomControlLayout.createParallelGroup(Alignment.LEADING) + .addGroup(jPanelBottomControlLayout.createSequentialGroup() .addComponent(RadioOneRowPerFile) .addGap(18, 18, 18) .addComponent(RadioOneRowPerIdentification) - .addGap(0, 0, Short.MAX_VALUE)) - .addGroup(jPanel1Layout.createSequentialGroup() - .addComponent(jCheckBoxQuoteAll) - .addPreferredGap(ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(jButtonSetAllColumns) - .addPreferredGap(ComponentPlacement.UNRELATED) - .addComponent(toggleColumnButton))) - .addContainerGap()) + .addContainerGap(GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGroup(Alignment.TRAILING, jPanelBottomControlLayout.createSequentialGroup() + .addGroup(jPanelBottomControlLayout.createParallelGroup(Alignment.TRAILING) + .addGroup(jPanelBottomControlLayout.createSequentialGroup() + .addComponent(jLabel1) + .addGap(18, 18, 18) + .addComponent(cmdEncoding, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(exportButton) + .addGap(18, 18, 18) + .addComponent(cancelButton, GroupLayout.PREFERRED_SIZE, 180, GroupLayout.PREFERRED_SIZE)) + .addGroup(jPanelBottomControlLayout.createSequentialGroup() + .addComponent(jCheckBoxQuoteAll) + .addPreferredGap(ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jButtonSetAllColumns) + .addGap(18, 18, 18) + .addComponent(toggleColumnButton))) + .addGap(27, 27, 27)))) ); - jPanel1Layout.setVerticalGroup(jPanel1Layout.createParallelGroup(Alignment.LEADING) - .addGroup(Alignment.TRAILING, jPanel1Layout.createSequentialGroup() - .addGroup(jPanel1Layout.createParallelGroup(Alignment.BASELINE) + jPanelBottomControlLayout.setVerticalGroup(jPanelBottomControlLayout.createParallelGroup(Alignment.LEADING) + .addGroup(Alignment.TRAILING, jPanelBottomControlLayout.createSequentialGroup() + .addGroup(jPanelBottomControlLayout.createParallelGroup(Alignment.BASELINE) .addComponent(toggleColumnButton) .addComponent(jCheckBoxQuoteAll) .addComponent(jButtonSetAllColumns)) .addPreferredGap(ComponentPlacement.RELATED) - .addGroup(jPanel1Layout.createParallelGroup(Alignment.BASELINE) + .addGroup(jPanelBottomControlLayout.createParallelGroup(Alignment.BASELINE) .addComponent(RadioOneRowPerFile) .addComponent(RadioOneRowPerIdentification)) .addPreferredGap(ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGroup(jPanel1Layout.createParallelGroup(Alignment.BASELINE) + .addGroup(jPanelBottomControlLayout.createParallelGroup(Alignment.BASELINE) .addComponent(cancelButton) .addComponent(exportButton) .addComponent(cmdEncoding, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) @@ -403,6 +450,14 @@ public void actionPerformed(ActionEvent evt) { .addContainerGap()) ); + jPanelRight.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 10)); + jPanelRight.setLayout(new BorderLayout()); + + jPanelCards.setBorder(BorderFactory.createEtchedBorder()); + jPanelCards.setLayout(new CardLayout()); + + jPanelColumnChoices.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + profileSelectLabel1.setText(NbBundle.getMessage(ExportDialog.class, "ExportDialog.profileSelectLabel1.text")); // NOI18N jCheckBoxId.setSelected(true); @@ -477,11 +532,11 @@ public void actionPerformed(ActionEvent evt) { jCheckBoxFileHash.setSelected(true); jCheckBoxFileHash.setText(NbBundle.getMessage(ExportDialog.class, "ExportDialog.jCheckBoxFileHash.text")); // NOI18N - GroupLayout jPanel2Layout = new GroupLayout(jPanel2); - jPanel2.setLayout(jPanel2Layout); - jPanel2Layout.setHorizontalGroup(jPanel2Layout.createParallelGroup(Alignment.LEADING) - .addGroup(jPanel2Layout.createSequentialGroup() - .addGroup(jPanel2Layout.createParallelGroup(Alignment.LEADING) + GroupLayout jPanelColumnChoicesLayout = new GroupLayout(jPanelColumnChoices); + jPanelColumnChoices.setLayout(jPanelColumnChoicesLayout); + jPanelColumnChoicesLayout.setHorizontalGroup(jPanelColumnChoicesLayout.createParallelGroup(Alignment.LEADING) + .addGroup(jPanelColumnChoicesLayout.createSequentialGroup() + .addGroup(jPanelColumnChoicesLayout.createParallelGroup(Alignment.LEADING) .addComponent(jCheckBoxId) .addComponent(profileSelectLabel1) .addComponent(jCheckBoxParentId) @@ -493,87 +548,129 @@ public void actionPerformed(ActionEvent evt) { .addComponent(jCheckBoxExtension) .addComponent(jCheckBoxExtMismatch)) .addPreferredGap(ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGroup(jPanel2Layout.createParallelGroup(Alignment.LEADING) - .addComponent(jCheckBoxFileHash) - .addGroup(jPanel2Layout.createParallelGroup(Alignment.LEADING) - .addComponent(jCheckBoxIdCount, Alignment.TRAILING) - .addComponent(jCheckBoxMIMEtype)) - .addComponent(jCheckBoxFormatName) - .addGroup(jPanel2Layout.createParallelGroup(Alignment.LEADING) - .addGroup(Alignment.TRAILING, jPanel2Layout.createParallelGroup(Alignment.LEADING) + .addGroup(jPanelColumnChoicesLayout.createParallelGroup(Alignment.LEADING) + .addGroup(jPanelColumnChoicesLayout.createParallelGroup(Alignment.LEADING) + .addComponent(jCheckBoxFileHash) + .addGroup(jPanelColumnChoicesLayout.createParallelGroup(Alignment.LEADING) + .addComponent(jCheckBoxIdCount, Alignment.TRAILING) + .addComponent(jCheckBoxMIMEtype)) + .addGroup(Alignment.TRAILING, jPanelColumnChoicesLayout.createParallelGroup(Alignment.LEADING) .addComponent(jCheckBoxIdMethod) .addComponent(jCheckBoxStatus) .addComponent(jCheckBoxFormatVersion)) - .addComponent(jCheckBoxPUID)) - .addComponent(jCheckBoxResourceType)) + .addComponent(jCheckBoxResourceType)) + .addComponent(jCheckBoxFormatName) + .addComponent(jCheckBoxPUID)) .addGap(0, 0, Short.MAX_VALUE)) ); - jPanel2Layout.setVerticalGroup(jPanel2Layout.createParallelGroup(Alignment.LEADING) - .addGroup(jPanel2Layout.createSequentialGroup() + jPanelColumnChoicesLayout.setVerticalGroup(jPanelColumnChoicesLayout.createParallelGroup(Alignment.LEADING) + .addGroup(jPanelColumnChoicesLayout.createSequentialGroup() .addComponent(profileSelectLabel1) .addPreferredGap(ComponentPlacement.RELATED) - .addGroup(jPanel2Layout.createParallelGroup(Alignment.BASELINE) + .addGroup(jPanelColumnChoicesLayout.createParallelGroup(Alignment.BASELINE) .addComponent(jCheckBoxId) .addComponent(jCheckBoxPUID)) .addPreferredGap(ComponentPlacement.RELATED) - .addGroup(jPanel2Layout.createParallelGroup(Alignment.BASELINE) + .addGroup(jPanelColumnChoicesLayout.createParallelGroup(Alignment.BASELINE) .addComponent(jCheckBoxParentId) .addComponent(jCheckBoxFormatName)) .addPreferredGap(ComponentPlacement.RELATED) - .addGroup(jPanel2Layout.createParallelGroup(Alignment.BASELINE) + .addGroup(jPanelColumnChoicesLayout.createParallelGroup(Alignment.BASELINE) .addComponent(jCheckBoxURI) .addComponent(jCheckBoxFormatVersion)) .addPreferredGap(ComponentPlacement.RELATED) - .addGroup(jPanel2Layout.createParallelGroup(Alignment.BASELINE) + .addGroup(jPanelColumnChoicesLayout.createParallelGroup(Alignment.BASELINE) .addComponent(jCheckBoxFilePath) .addComponent(jCheckBoxMIMEtype)) .addPreferredGap(ComponentPlacement.RELATED) - .addGroup(jPanel2Layout.createParallelGroup(Alignment.BASELINE) + .addGroup(jPanelColumnChoicesLayout.createParallelGroup(Alignment.BASELINE) .addComponent(jCheckBoxFileName) .addComponent(jCheckBoxIdCount)) .addPreferredGap(ComponentPlacement.RELATED) - .addGroup(jPanel2Layout.createParallelGroup(Alignment.BASELINE) + .addGroup(jPanelColumnChoicesLayout.createParallelGroup(Alignment.BASELINE) .addComponent(jCheckBoxFileSize, GroupLayout.PREFERRED_SIZE, 24, GroupLayout.PREFERRED_SIZE) .addComponent(jCheckBoxIdMethod)) .addPreferredGap(ComponentPlacement.RELATED) - .addGroup(jPanel2Layout.createParallelGroup(Alignment.BASELINE) + .addGroup(jPanelColumnChoicesLayout.createParallelGroup(Alignment.BASELINE) .addComponent(jCheckBoxLastModified) .addComponent(jCheckBoxStatus)) .addPreferredGap(ComponentPlacement.RELATED) - .addGroup(jPanel2Layout.createParallelGroup(Alignment.BASELINE) + .addGroup(jPanelColumnChoicesLayout.createParallelGroup(Alignment.BASELINE) .addComponent(jCheckBoxExtension) .addComponent(jCheckBoxResourceType)) .addPreferredGap(ComponentPlacement.RELATED) - .addGroup(jPanel2Layout.createParallelGroup(Alignment.BASELINE) + .addGroup(jPanelColumnChoicesLayout.createParallelGroup(Alignment.BASELINE) .addComponent(jCheckBoxExtMismatch) .addComponent(jCheckBoxFileHash)) - .addGap(0, 9, Short.MAX_VALUE)) + .addGap(0, 52, Short.MAX_VALUE)) ); + jPanelColumnChoicesLayout.linkSize(SwingConstants.VERTICAL, new Component[] {jCheckBoxExtMismatch, jCheckBoxExtension, jCheckBoxFileHash, jCheckBoxFileName, jCheckBoxFilePath, jCheckBoxFileSize, jCheckBoxFormatName, jCheckBoxFormatVersion, jCheckBoxId, jCheckBoxIdCount, jCheckBoxIdMethod, jCheckBoxLastModified, jCheckBoxMIMEtype, jCheckBoxPUID, jCheckBoxParentId, jCheckBoxResourceType, jCheckBoxStatus, jCheckBoxURI}); + + jPanelCards.add(jPanelColumnChoices, "card5"); + + jPanelTemplateChoices.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + jLabel2.setText(NbBundle.getMessage(ExportDialog.class, "ExportDialog.jLabel2.text")); // NOI18N + + jComboBox1.setModel(new DefaultComboBoxModel<>(new String[] { "Item 1", "Item 2", "Item 3", "Item 4" })); + + GroupLayout jPanelTemplateChoicesLayout = new GroupLayout(jPanelTemplateChoices); + jPanelTemplateChoices.setLayout(jPanelTemplateChoicesLayout); + jPanelTemplateChoicesLayout.setHorizontalGroup(jPanelTemplateChoicesLayout.createParallelGroup(Alignment.LEADING) + .addGroup(jPanelTemplateChoicesLayout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanelTemplateChoicesLayout.createParallelGroup(Alignment.LEADING) + .addComponent(jLabel2) + .addComponent(jComboBox1, 0, 638, Short.MAX_VALUE)) + .addGap(25, 25, 25)) + ); + jPanelTemplateChoicesLayout.setVerticalGroup(jPanelTemplateChoicesLayout.createParallelGroup(Alignment.LEADING) + .addGroup(jPanelTemplateChoicesLayout.createSequentialGroup() + .addGap(25, 25, 25) + .addComponent(jLabel2) + .addGap(32, 32, 32) + .addComponent(jComboBox1, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addContainerGap(389, Short.MAX_VALUE)) + ); + + jPanelCards.add(jPanelTemplateChoices, "card3"); + + jPanelRight.add(jPanelCards, BorderLayout.CENTER); + + jCheckBoxUseTemplate.setText(NbBundle.getMessage(ExportDialog.class, "ExportDialog.jCheckBoxUseTemplate.text")); // NOI18N + jCheckBoxUseTemplate.setBorder(BorderFactory.createEmptyBorder(1, 15, 1, 1)); + jCheckBoxUseTemplate.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent evt) { + jCheckBoxUseTemplateStateChanged(evt); + } + }); + jPanelRight.add(jCheckBoxUseTemplate, BorderLayout.PAGE_START); + GroupLayout layout = new GroupLayout(getContentPane()); getContentPane().setLayout(layout); layout.setHorizontalGroup(layout.createParallelGroup(Alignment.LEADING) - .addComponent(jPanel1, Alignment.TRAILING, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jPanelBottomControl, Alignment.TRAILING, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addGroup(layout.createSequentialGroup() .addContainerGap() .addGroup(layout.createParallelGroup(Alignment.LEADING) .addComponent(profileSelectLabel) - .addComponent(jScrollPane1, GroupLayout.PREFERRED_SIZE, 300, GroupLayout.PREFERRED_SIZE)) - .addGap(18, 18, Short.MAX_VALUE) - .addComponent(jPanel2, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addComponent(jScrollPaneProfileSelection, GroupLayout.DEFAULT_SIZE, 375, Short.MAX_VALUE)) + .addGap(18, 18, 18) + .addComponent(jPanelRight, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addContainerGap()) ); layout.setVerticalGroup(layout.createParallelGroup(Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addContainerGap() - .addGroup(layout.createParallelGroup(Alignment.LEADING) + .addGroup(layout.createParallelGroup(Alignment.TRAILING) .addGroup(layout.createSequentialGroup() .addComponent(profileSelectLabel) .addPreferredGap(ComponentPlacement.RELATED) - .addComponent(jScrollPane1, GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE)) - .addComponent(jPanel2, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addComponent(jScrollPaneProfileSelection, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addComponent(jPanelRight, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) .addPreferredGap(ComponentPlacement.RELATED) - .addComponent(jPanel1, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)) + .addComponent(jPanelBottomControl, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)) ); }// //GEN-END:initComponents @@ -635,6 +732,13 @@ private void jButtonSetAllColumnsActionPerformed(ActionEvent evt) {//GEN-FIRST:e jCheckBoxFileHash.setSelected(true); }//GEN-LAST:event_jButtonSetAllColumnsActionPerformed + private void jCheckBoxUseTemplateStateChanged(ChangeEvent evt) {//GEN-FIRST:event_jCheckBoxUseTemplateStateChanged + jPanelTemplateChoices.setVisible(jCheckBoxUseTemplate.isSelected()); + jPanelColumnChoices.setVisible(!jCheckBoxUseTemplate.isSelected()); + jButtonSetAllColumns.setEnabled(!jCheckBoxUseTemplate.isSelected()); + toggleColumnButton.setEnabled(!jCheckBoxUseTemplate.isSelected()); + }//GEN-LAST:event_jCheckBoxUseTemplateStateChanged + /** * @param evt The event that triggers the action. */ @@ -673,10 +777,16 @@ protected void exportButtonActionPerformed(ActionEvent evt) { private JCheckBox jCheckBoxResourceType; private JCheckBox jCheckBoxStatus; private JCheckBox jCheckBoxURI; + private JCheckBox jCheckBoxUseTemplate; + private JComboBox jComboBox1; private JLabel jLabel1; - private JPanel jPanel1; - private JPanel jPanel2; - private JScrollPane jScrollPane1; + private JLabel jLabel2; + private JPanel jPanelBottomControl; + private JPanel jPanelCards; + private JPanel jPanelColumnChoices; + private JPanel jPanelRight; + private JPanel jPanelTemplateChoices; + private JScrollPane jScrollPaneProfileSelection; private JLabel profileSelectLabel; private JLabel profileSelectLabel1; private JTable profileSelectTable; @@ -775,7 +885,54 @@ private ComboBoxModel getOutputEncodings() { return model; } - + + class ExportTemplateComboBoxItem implements Comparable{ + private final String label; + private final Path templatePath; + + ExportTemplateComboBoxItem(Path templateFilePath) { + String templateFileName = templateFilePath.getFileName().toString(); + this.label = templateFileName.substring(0, templateFileName.length() - EXPORT_TEMPLATE_FILE_EXTENSION.length()); + this.templatePath = templateFilePath; + } + + public String getLabel() { + return label; + } + + public Path getTemplatePath() { + return templatePath; + } + + @Override + public String toString() { + return this.label; + } + + @Override + public int compareTo(Object o) { + if (!(o instanceof ExportTemplateComboBoxItem)) { + throw new ClassCastException("Invalid item for comparison " + o.getClass().getName()); + } + ExportTemplateComboBoxItem that = (ExportTemplateComboBoxItem) o; + return this.getLabel().toUpperCase().compareTo(that.getLabel().toUpperCase()); + } + } + + private ComboBoxModel getExportTemplatesModel() { + List items = new LinkedList<>(); + try (Stream fileStream = Files.list(exportTemplatesFolder)) { + List templates = fileStream.collect(Collectors.toList()); + for (Path template : templates) { + if (!Files.isDirectory(template) && (template.getFileName().toString().endsWith(EXPORT_TEMPLATE_FILE_EXTENSION))) { + items.add(new ExportTemplateComboBoxItem(template)); + } + } + return new SortedComboBoxModel(items) ; + } catch (IOException e) { + throw new RuntimeException(e); + } + } private final class CheckBoxRenderer extends JCheckBox implements TableCellRenderer { diff --git a/droid-swing-ui/src/main/java/uk/gov/nationalarchives/droid/gui/util/SortedComboBoxModel.java b/droid-swing-ui/src/main/java/uk/gov/nationalarchives/droid/gui/util/SortedComboBoxModel.java new file mode 100644 index 000000000..0041fd3c2 --- /dev/null +++ b/droid-swing-ui/src/main/java/uk/gov/nationalarchives/droid/gui/util/SortedComboBoxModel.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2016, The National Archives + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the The National Archives nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package uk.gov.nationalarchives.droid.gui.util; + +import javax.swing.DefaultComboBoxModel; +import java.util.Collections; +import java.util.List; + +/** + * Model to make the items in the combo box appear sorted. + * Items need to implement Comparable + * @param ComboBox Item that implements comparable + */ +public class SortedComboBoxModel> extends DefaultComboBoxModel { + public SortedComboBoxModel() { + super(); + } + + public SortedComboBoxModel(List items) { + Collections.sort(items); + for (int i = 0; i < items.size(); i++) { + super.addElement(items.get(i)); + } + } +} diff --git a/droid-swing-ui/src/main/resources/uk/gov/nationalarchives/droid/gui/export/Bundle.properties b/droid-swing-ui/src/main/resources/uk/gov/nationalarchives/droid/gui/export/Bundle.properties index cfd52f5b7..7b9e569dd 100644 --- a/droid-swing-ui/src/main/resources/uk/gov/nationalarchives/droid/gui/export/Bundle.properties +++ b/droid-swing-ui/src/main/resources/uk/gov/nationalarchives/droid/gui/export/Bundle.properties @@ -71,3 +71,5 @@ ExportDialog.toggleColumnButton.toolTipText=Toggles columns to export between se ExportDialog.toggleColumnButton.text=Toggle columns ExportDialog.jButtonSetAllColumns.text=Set all columns ExportDialog.jButtonSetAllColumns.toolTipText=Sets all columns for export +ExportDialog.jLabel2.text=Select an export template: +ExportDialog.jCheckBoxUseTemplate.text=Use export template diff --git a/droid-swing-ui/src/test/java/uk/gov/nationalarchives/droid/gui/util/SortedComboBoxModelTest.java b/droid-swing-ui/src/test/java/uk/gov/nationalarchives/droid/gui/util/SortedComboBoxModelTest.java new file mode 100644 index 000000000..ab4428d78 --- /dev/null +++ b/droid-swing-ui/src/test/java/uk/gov/nationalarchives/droid/gui/util/SortedComboBoxModelTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2016, The National Archives + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the The National Archives nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package uk.gov.nationalarchives.droid.gui.util; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class SortedComboBoxModelTest { + @Test + public void should_show_the_string_entries_in_sorted_order() { + List items = Arrays.asList("First Item", "0th item", "last item", "First item"); + SortedComboBoxModel model = new SortedComboBoxModel<>(items); + assertEquals("0th item", model.getElementAt(0)); + assertEquals("First Item", model.getElementAt(1)); + assertEquals("First item", model.getElementAt(2)); + assertEquals("last item", model.getElementAt(3)); + } + + @Test + public void should_show_the_numeric_entries_in_sorted_order() { + List items = Arrays.asList(12, 3, 4, 0); + SortedComboBoxModel model = new SortedComboBoxModel<>(items); + assertEquals(0, model.getElementAt(0)); + assertEquals(3, model.getElementAt(1)); + assertEquals(4, model.getElementAt(2)); + assertEquals(12, model.getElementAt(3)); + } + + @Test + public void should_show_the_entries_based_on_custom_sorted_order_based_on_comparable_implemntation() { + List items = Arrays.asList(new IntAsStringSortedItem(12), + new IntAsStringSortedItem(3), + new IntAsStringSortedItem(4), + new IntAsStringSortedItem(0)); + SortedComboBoxModel model = new SortedComboBoxModel<>(items); + assertEquals("0", ((IntAsStringSortedItem)model.getElementAt(0)).valAsString); + assertEquals("12", ((IntAsStringSortedItem)model.getElementAt(1)).valAsString); + assertEquals("3", ((IntAsStringSortedItem)model.getElementAt(2)).valAsString); + assertEquals("4", ((IntAsStringSortedItem)model.getElementAt(3)).valAsString); + } + + static class IntAsStringSortedItem implements Comparable { + private final String valAsString; + + IntAsStringSortedItem(Integer someVal) { + this.valAsString = someVal.toString(); + } + @Override + public int compareTo(Object o) { + IntAsStringSortedItem that = (IntAsStringSortedItem) o; + return this.valAsString.compareTo(that.valAsString); + } + } +} \ No newline at end of file