diff --git a/community/shell/src/main/java/org/neo4j/shell/TextUtil.java b/community/common/src/main/java/org/neo4j/helpers/TextUtil.java similarity index 79% rename from community/shell/src/main/java/org/neo4j/shell/TextUtil.java rename to community/common/src/main/java/org/neo4j/helpers/TextUtil.java index 9b8c9957b728..73fd84ceff89 100644 --- a/community/shell/src/main/java/org/neo4j/shell/TextUtil.java +++ b/community/common/src/main/java/org/neo4j/helpers/TextUtil.java @@ -17,7 +17,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.neo4j.shell; +package org.neo4j.helpers; import java.util.ArrayList; import java.util.Collection; @@ -44,7 +44,7 @@ public static String templateString( String templateString, { // Sort data strings on length. Map> lengthMap = - new HashMap>(); + new HashMap<>(); int longest = 0; for ( String key : data.keySet() ) { @@ -62,7 +62,7 @@ public static String templateString( String templateString, } else { - innerList = new ArrayList(); + innerList = new ArrayList<>(); lengthMap.put( innerKey, innerList ); } innerList.add( key ); @@ -116,17 +116,17 @@ public static String lastWordOrQuoteOf( String text, boolean preserveQuotation ) return lastWord; } - public static String[] splitAndKeepEscapedSpaces( String string, boolean preserveEscapes ) + private static String[] splitAndKeepEscapedSpaces( String string, boolean preserveEscapes ) { - Collection result = new ArrayList(); + Collection result = new ArrayList<>(); StringBuilder current = new StringBuilder(); for ( int i = 0; i < string.length(); i++ ) { char ch = string.charAt( i ); if ( ch == ' ' ) { - boolean isGluedSpace = i > 0 && string.charAt( i - 1 ) == '\\'; - if ( !isGluedSpace ) + boolean isEscapedSpace = i > 0 && string.charAt( i - 1 ) == '\\'; + if ( !isEscapedSpace ) { result.add( current.toString() ); current = new StringBuilder(); @@ -146,29 +146,6 @@ public static String[] splitAndKeepEscapedSpaces( String string, boolean preserv return result.toArray( new String[result.size()] ); } - public static String multiplyString( String string, int times ) - { - StringBuilder result = new StringBuilder(); - for ( int i = 0; i < times; i++ ) - { - result.append( string ); - } - return result.toString(); - } - - public static String removeSpaces( String command ) - { - while ( command.length() > 0 && command.charAt( 0 ) == ' ' ) - { - command = command.substring( 1 ); - } - while ( command.length() > 0 && command.charAt( command.length() - 1 ) == ' ' ) - { - command = command.substring( 0, command.length() - 1 ); - } - return command; - } - /** * Tokenizes a string, regarding quotes. * @@ -177,7 +154,7 @@ public static String removeSpaces( String command ) */ public static String[] tokenizeStringWithQuotes( String string ) { - return tokenizeStringWithQuotes( string, true ); + return tokenizeStringWithQuotes( string, true, false ); } /** @@ -189,15 +166,16 @@ public static String[] tokenizeStringWithQuotes( String string ) * * @param string the string to tokenize. * @param trim whether or not to trim each token. + * @param preserveEscapeCharacters whether or not to preserve escape characters '\', otherwise skip them. * @return the tokens from the line. */ - public static String[] tokenizeStringWithQuotes( String string, boolean trim ) + public static String[] tokenizeStringWithQuotes( String string, boolean trim, boolean preserveEscapeCharacters ) { if ( trim ) { string = string.trim(); } - ArrayList result = new ArrayList(); + ArrayList result = new ArrayList<>(); string = string.trim(); boolean inside = string.startsWith( "\"" ); StringTokenizer quoteTokenizer = new StringTokenizer( string, "\"" ); @@ -219,22 +197,10 @@ else if ( inside ) } else { - Collections.addAll( result, TextUtil.splitAndKeepEscapedSpaces( token, false ) ); + Collections.addAll( result, TextUtil.splitAndKeepEscapedSpaces( token, preserveEscapeCharacters ) ); } inside = !inside; } return result.toArray( new String[result.size()] ); } - - public static String stripFromQuotes( String string ) - { - if ( string != null ) - { - if ( string.startsWith( "\"" ) && string.endsWith( "\"" ) ) - { - return string.substring( 1, string.length() - 1 ); - } - } - return string; - } } diff --git a/community/common/src/test/java/org/neo4j/helpers/TextUtilTest.java b/community/common/src/test/java/org/neo4j/helpers/TextUtilTest.java new file mode 100644 index 000000000000..65ecd86fcf08 --- /dev/null +++ b/community/common/src/test/java/org/neo4j/helpers/TextUtilTest.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.helpers; + +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +public class TextUtilTest +{ + @Test + public void shouldReplaceVariablesWithValuesInTemplateString() throws Exception + { + // given + String template = "This is a $FIRST that $SECOND $THIRD!"; + Map values = new HashMap<>(); + values.put( "FIRST", "String" ); + values.put( "SECOND", "should" ); + values.put( "THIRD", "act as a template!" ); + + // when + String string = TextUtil.templateString( template, values ); + + // then + assertEquals( "This is a String that should act as a template!!", string ); + } + + @Test + public void shouldTokenizeStringWithWithoutQuotes() throws Exception + { + // given + String untokenized = "First Second Third"; + + // when + String[] tokenized = TextUtil.tokenizeStringWithQuotes( untokenized ); + + // then + assertArrayEquals( new String[] {"First", "Second", "Third"}, tokenized ); + } + + @Test + public void shouldTokenizeStringWithQuotes() throws Exception + { + // given + String untokenized = "First \"Second one\" Third \"And a fourth\""; + + // when + String[] tokenized = TextUtil.tokenizeStringWithQuotes( untokenized ); + + // then + assertArrayEquals( new String[] {"First", "Second one", "Third", "And a fourth"}, tokenized ); + } + + @Test + public void shouldTokenStringWithWithQuotesAndEscapedSpaces() throws Exception + { + // given + String untokenized = "First \"Second one\" Third And\\ a\\ fourth"; + + // when + String[] tokenized = TextUtil.tokenizeStringWithQuotes( untokenized ); + + // then + assertArrayEquals( new String[] {"First", "Second one", "Third", "And a fourth"}, tokenized ); + } + + @Test + public void shouldPreserveBackslashes() throws Exception + { + // given + String untokenized = "First C:\\a\\b\\c"; + + // when + String[] tokenized = TextUtil.tokenizeStringWithQuotes( untokenized, true, true ); + + // then + assertArrayEquals( new String[] {"First", "C:\\a\\b\\c"}, tokenized ); + } +} diff --git a/community/dbms/src/main/java/org/neo4j/commandline/dbms/ImportCommand.java b/community/dbms/src/main/java/org/neo4j/commandline/dbms/ImportCommand.java index 2eb17447511a..7be595cf408c 100644 --- a/community/dbms/src/main/java/org/neo4j/commandline/dbms/ImportCommand.java +++ b/community/dbms/src/main/java/org/neo4j/commandline/dbms/ImportCommand.java @@ -20,6 +20,7 @@ package org.neo4j.commandline.dbms; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; @@ -41,6 +42,7 @@ import org.neo4j.kernel.impl.util.Validators; import static org.neo4j.csv.reader.Configuration.DEFAULT; +import static org.neo4j.tooling.ImportTool.parseFileArgumentList; import static org.neo4j.unsafe.impl.batchimport.Configuration.DEFAULT_MAX_MEMORY_PERCENT; import static org.neo4j.unsafe.impl.batchimport.input.csv.Configuration.COMMAS; @@ -58,9 +60,8 @@ public String usage() } } ) .withDatabase() - .withAdditionalConfig() - .withArgument( new OptionalNamedArg( "from", "source-directory", "", - "The location of the pre-3.0 database (e.g. /data/graph.db)." ) ); + .withAdditionalConfig(); + private static final Arguments csvArguments = new Arguments() .withArgument( new OptionalNamedArg( "mode", "csv", "csv", "Import a collection of CSV files." ) { @@ -71,70 +72,24 @@ public String usage() } } ) .withDatabase() - .withAdditionalConfig() - .withArgument( new OptionalNamedArg( "report-file", "filename", DEFAULT_REPORT_FILE_NAME, - "File in which to store the report of the csv-import." ) ) - .withArgument( new OptionalNamedArg( "nodes[:Label1:Label2]", "\"file1,file2,...\"", "", - "Node CSV header and data. Multiple files will be logically seen as " + - "one big file from the perspective of the importer. The first line " + - "must contain the header. Multiple data sources like these can be " + - "specified in one import, where each data source has its own header. " + - "Note that file groups must be enclosed in quotation marks." ) ) - .withArgument( new OptionalNamedArg( "relationships[:RELATIONSHIP_TYPE]", "\"file1,file2,...\"", "", - "Relationship CSV header and data. Multiple files will be logically " + - "seen as one big file from the perspective of the importer. The first " + - "line must contain the header. Multiple data sources like these can be " + - "specified in one import, where each data source has its own header. " + - "Note that file groups must be enclosed in quotation marks." ) ) - .withArgument( new OptionalNamedArg( "id-type", new String[]{"STRING", "INTEGER", "ACTUAL"}, - "STRING", "Each node must provide a unique id. This is used to find the correct " + - "nodes when creating relationships. Possible values are\n" + - " STRING: arbitrary strings for identifying nodes,\n" + - " INTEGER: arbitrary integer values for identifying nodes,\n" + - " ACTUAL: (advanced) actual node ids.\n" + - "For more information on id handling, please see the Neo4j Manual: " + - "https://neo4j.com/docs/operations-manual/current/tools/import/" ) ) - .withArgument( new OptionalNamedArg( "input-encoding", "character-set", "UTF-8", - "Character set that input data is encoded in." ) ) - .withArgument( new OptionalBooleanArg( "ignore-extra-columns", false, - "If un-specified columns should be ignored during the import." ) ) - .withArgument( new OptionalBooleanArg( "ignore-duplicate-nodes", false, - "If duplicate nodes should be ignored during the import." ) ) - .withArgument( new OptionalBooleanArg( "ignore-missing-nodes", false, - "If relationships referring to missing nodes should be ignored during the import." ) ) - .withArgument( new OptionalBooleanArg( "multiline-fields", - DEFAULT.multilineFields(), - "Whether or not fields from input source can span multiple lines," + - " i.e. contain newline characters." ) ) - .withArgument( new OptionalNamedArg( "delimiter", - "delimiter-character", - String.valueOf( COMMAS.delimiter() ), - "Delimiter character between values in CSV data." ) ) - .withArgument( new OptionalNamedArg( "array-delimiter", - "array-delimiter-character", - String.valueOf( COMMAS.arrayDelimiter() ), - "Delimiter character between array elements within a value in CSV data." ) ) - .withArgument( new OptionalNamedArg( "quote", - "quotation-character", - String.valueOf( COMMAS.quotationCharacter() ), - "Character to treat as quotation character for values in CSV data. " - + "Quotes can be escaped as per RFC 4180 by doubling them, for example \"\" would be " + - "interpreted as a literal \". You cannot escape using \\." ) ) - .withArgument( new OptionalNamedArg( "max-memory", - "max-memory-that-importer-can-use", - String.valueOf( DEFAULT_MAX_MEMORY_PERCENT ) + "%", - "Maximum memory that neo4j-admin can use for various data structures and caching " + - "to improve performance. " + - "Values can be plain numbers, like 10000000 or e.g. 20G for 20 gigabyte, or even e.g. 70%" + - "." ) ); + .withAdditionalConfig(); private static final Arguments allArguments = new Arguments() .withDatabase() .withAdditionalConfig() .withArgument( new OptionalNamedArg( "mode", allowedModes, "csv", - "Import a collection of CSV files or a pre-3.0 installation." ) ) + "Import a collection of CSV files or a pre-3.0 installation." ) ); + + private static void includeDatabaseArguments( Arguments arguments ) + { + arguments .withArgument( new OptionalNamedArg( "from", "source-directory", "", - "The location of the pre-3.0 database (e.g. /data/graph.db)." ) ) + "The location of the pre-3.0 database (e.g. /data/graph.db)." ) ); + } + + private static void includeCsvArguments( Arguments arguments ) + { + arguments .withArgument( new OptionalNamedArg( "report-file", "filename", DEFAULT_REPORT_FILE_NAME, "File in which to store the report of the csv-import." ) ) .withArgument( new OptionalNamedArgWithMetadata( "nodes", @@ -144,7 +99,7 @@ public String usage() "one big file from the perspective of the importer. The first line " + "must contain the header. Multiple data sources like these can be " + "specified in one import, where each data source has its own header. " + - "Note that file groups must be enclosed in quotation marks." ) ) + "Note that file groups must be enclosed in quotation marks." ) ) .withArgument( new OptionalNamedArgWithMetadata( "relationships", ":RELATIONSHIP_TYPE", "\"file1,file2,...\"", @@ -153,7 +108,7 @@ public String usage() "seen as one big file from the perspective of the importer. The first " + "line must contain the header. Multiple data sources like these can be " + "specified in one import, where each data source has its own header. " + - "Note that file groups must be enclosed in quotation marks." ) ) + "Note that file groups must be enclosed in quotation marks." ) ) .withArgument( new OptionalNamedArg( "id-type", new String[]{"STRING", "INTEGER", "ACTUAL"}, "STRING", "Each node must provide a unique id. This is used to find the correct " + "nodes when creating relationships. Possible values are:\n" + @@ -175,10 +130,11 @@ public String usage() "Whether or not fields from input source can span multiple lines," + " i.e. contain newline characters." ) ) .withArgument( new OptionalNamedArg( "delimiter", + "delimiter-character", String.valueOf( COMMAS.delimiter() ), - String.valueOf( COMMAS.delimiter() ), "Delimiter character between values in CSV data." ) ) + "Delimiter character between values in CSV data." ) ) .withArgument( new OptionalNamedArg( "array-delimiter", - String.valueOf( COMMAS.delimiter() ), + "array-delimiter-character", String.valueOf( COMMAS.arrayDelimiter() ), "Delimiter character between array elements within a value in CSV data." ) ) .withArgument( new OptionalNamedArg( "quote", @@ -193,7 +149,25 @@ public String usage() "Maximum memory that neo4j-admin can use for various data structures and caching " + "to improve performance. " + "Values can be plain numbers, like 10000000 or e.g. 20G for 20 gigabyte, or even e.g. 70%" + - "." ) ); + "." ) ) + .withArgument( new OptionalNamedArg( "f", + "File containing all arguments to this import", + "", + "File containing all arguments, used as an alternative to supplying all arguments on the command line directly." + + "Each argument can be on a separate line or multiple arguments per line separated by space." + + "Arguments containing spaces needs to be quoted." + + "Supplying other arguments in addition to this file argument is not supported." ) ); + } + + static + { + includeDatabaseArguments( databaseArguments ); + includeDatabaseArguments( allArguments ); + + includeCsvArguments( csvArguments ); + includeCsvArguments( allArguments ); + } + public static Arguments databaseArguments() { return databaseArguments; @@ -236,7 +210,12 @@ public void execute( String[] args ) throws IncorrectUsage, CommandFailed try { - mode = allArguments.parse( args ).get("mode" ); + mode = allArguments.parse( args ).get( "mode" ); + Optional fileArgument = allArguments.getOptionalPath( "f" ); + if ( fileArgument.isPresent() ) + { + allArguments.parse( parseFileArgumentList( fileArgument.get().toFile() ) ); + } database = allArguments.get( "database" ); additionalConfigFile = allArguments.getOptionalPath( "additional-config" ); } @@ -244,6 +223,10 @@ public void execute( String[] args ) throws IncorrectUsage, CommandFailed { throw new IncorrectUsage( e.getMessage() ); } + catch ( IOException e ) + { + throw new UncheckedIOException( e ); + } try { @@ -261,11 +244,11 @@ public void execute( String[] args ) throws IncorrectUsage, CommandFailed } catch ( IOException e ) { - throw new RuntimeException( e ); + throw new UncheckedIOException( e ); } } - private Map loadAdditionalConfig( Optional additionalConfigFile ) + private static Map loadAdditionalConfig( Optional additionalConfigFile ) { if ( additionalConfigFile.isPresent() ) { diff --git a/community/dbms/src/test/java/org/neo4j/commandline/dbms/ImportCommandTest.java b/community/dbms/src/test/java/org/neo4j/commandline/dbms/ImportCommandTest.java index 9cff1fedfc87..5c4b9fb39733 100644 --- a/community/dbms/src/test/java/org/neo4j/commandline/dbms/ImportCommandTest.java +++ b/community/dbms/src/test/java/org/neo4j/commandline/dbms/ImportCommandTest.java @@ -213,6 +213,7 @@ public void shouldPrintNiceHelp() throws Throwable " [--array-delimiter=]%n" + " [--quote=]%n" + " [--max-memory=]%n" + + " [--f=]%n" + "usage: neo4j-admin import --mode=database [--database=]%n" + " [--additional-config=]%n" + " [--from=]%n" + @@ -274,9 +275,9 @@ public void shouldPrintNiceHelp() throws Throwable " --multiline-fields=%n" + " Whether or not fields from input source can span multiple lines, i.e.%n" + " contain newline characters. [default:false]%n" + - " --delimiter=<,>%n" + + " --delimiter=%n" + " Delimiter character between values in CSV data. [default:,]%n" + - " --array-delimiter=<,>%n" + + " --array-delimiter=%n" + " Delimiter character between array elements within a value in CSV data.%n" + " [default:;]%n" + " --quote=%n" + @@ -286,7 +287,13 @@ public void shouldPrintNiceHelp() throws Throwable " --max-memory=%n" + " Maximum memory that neo4j-admin can use for various data structures and%n" + " caching to improve performance. Values can be plain numbers, like 10000000%n" + - " or e.g. 20G for 20 gigabyte, or even e.g. 70%%. [default:90%%]%n"), + " or e.g. 20G for 20 gigabyte, or even e.g. 70%%. [default:90%%]%n" + + " --f=%n" + + " File containing all arguments, used as an alternative to supplying all%n" + + " arguments on the command line directly.Each argument can be on a separate%n" + + " line or multiple arguments per line separated by space.Arguments%n" + + " containing spaces needs to be quoted.Supplying other arguments in addition%n" + + " to this file argument is not supported. [default:]%n" ), baos.toString() ); } } diff --git a/community/import-tool/src/main/java/org/neo4j/tooling/ImportTool.java b/community/import-tool/src/main/java/org/neo4j/tooling/ImportTool.java index b43bae798cfa..daef03cc9054 100644 --- a/community/import-tool/src/main/java/org/neo4j/tooling/ImportTool.java +++ b/community/import-tool/src/main/java/org/neo4j/tooling/ImportTool.java @@ -25,8 +25,10 @@ import java.io.OutputStream; import java.io.PrintStream; import java.nio.charset.Charset; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.List; import java.util.Map.Entry; import java.util.function.Function; @@ -72,13 +74,16 @@ import org.neo4j.unsafe.impl.batchimport.staging.ExecutionMonitors; import static java.nio.charset.Charset.defaultCharset; +import static java.util.Arrays.asList; import static org.neo4j.graphdb.factory.GraphDatabaseSettings.logs_directory; import static org.neo4j.graphdb.factory.GraphDatabaseSettings.store_internal_log_path; import static org.neo4j.helpers.Exceptions.launderedException; import static org.neo4j.helpers.Format.bytes; import static org.neo4j.helpers.Strings.TAB; +import static org.neo4j.helpers.TextUtil.tokenizeStringWithQuotes; import static org.neo4j.io.ByteUnit.mebiBytes; +import static org.neo4j.io.fs.FileUtils.readTextFile; import static org.neo4j.kernel.configuration.Settings.parseLongWithUnit; import static org.neo4j.kernel.impl.util.Converters.withDefault; import static org.neo4j.unsafe.impl.batchimport.AdditionalInitialIds.EMPTY; @@ -118,6 +123,12 @@ public class ImportTool enum Options { + FILE( "f", null, + "", + "File containing all arguments, used as an alternative to supplying all arguments on the command line directly." + + "Each argument can be on a separate line or multiple arguments per line separated by space." + + "Arguments containing spaces needs to be quoted." + + "Supplying other arguments in addition to this file argument is not supported." ), STORE_DIR( "into", null, "", "Database directory to import into. " + "Must not contain existing database." ), @@ -266,9 +277,7 @@ enum Options this( key, defaultValue, usage, description, supported, false ); } - Options( String key, Object defaultValue, String usage, String description, boolean supported, boolean - keyAndUsageGoTogether - ) + Options( String key, Object defaultValue, String usage, String description, boolean supported, boolean keyAndUsageGoTogether ) { this.key = key; this.defaultValue = defaultValue; @@ -408,6 +417,8 @@ public static void main( String[] incomingArguments, boolean defaultSettingsSuit boolean success = false; try ( FileSystemAbstraction fs = new DefaultFileSystemAbstraction() ) { + args = useArgumentsFromFileArgumentIfPresent( args ); + storeDir = args.interpretOption( Options.STORE_DIR.key(), Converters.mandatory(), Converters.toFile(), Validators.DIRECTORY_IS_WRITABLE, Validators.CONTAINS_NO_EXISTING_DATABASE ); Config config = Config.defaults( GraphDatabaseSettings.neo4j_home, storeDir.getAbsolutePath() ); @@ -482,6 +493,31 @@ idType, csvConfiguration( args, defaultSettingsSuitableForTests ), badCollector, } } + public static Args useArgumentsFromFileArgumentIfPresent( Args args ) throws IOException + { + String fileArgument = args.get( Options.FILE.key(), null ); + if ( fileArgument != null ) + { + // Are there any other arguments supplied, in addition to this -f argument? + if ( args.asMap().size() > 1 ) + { + throw new IllegalArgumentException( + "Supplying arguments in addition to " + Options.FILE.argument() + " isn't supported." ); + } + + // Read the arguments from the -f file and use those instead + args = Args.parse( parseFileArgumentList( new File( fileArgument ) ) ); + } + return args; + } + + public static String[] parseFileArgumentList( File file ) throws IOException + { + List arguments = new ArrayList<>(); + readTextFile( file, line -> arguments.addAll( asList( tokenizeStringWithQuotes( line, true, true ) ) ) ); + return arguments.toArray( new String[arguments.size()] ); + } + static Long parseMaxMemory( String maxMemoryString ) { if ( maxMemoryString != null ) diff --git a/community/import-tool/src/test/java/org/neo4j/tooling/ImportToolTest.java b/community/import-tool/src/test/java/org/neo4j/tooling/ImportToolTest.java index 261b1ff6ce7d..bfa29daf5d28 100644 --- a/community/import-tool/src/test/java/org/neo4j/tooling/ImportToolTest.java +++ b/community/import-tool/src/test/java/org/neo4j/tooling/ImportToolTest.java @@ -89,6 +89,7 @@ import static org.neo4j.helpers.collection.Iterators.count; import static org.neo4j.helpers.collection.MapUtil.store; import static org.neo4j.helpers.collection.MapUtil.stringMap; +import static org.neo4j.io.fs.FileUtils.writeToFile; import static org.neo4j.kernel.impl.store.MetaDataStore.DEFAULT_NAME; import static org.neo4j.tooling.ImportTool.MULTI_FILE_DELIMITER; import static org.neo4j.unsafe.impl.batchimport.Configuration.BAD_FILE_NAME; @@ -1906,6 +1907,57 @@ public void shouldKeepStoreFilesAfterFailedImport() throws Exception } } + @Test + public void shouldSupplyArgumentsAsFile() throws Exception + { + // given + List nodeIds = nodeIds(); + Configuration config = Configuration.COMMAS; + File argumentFile = file( "args" ); + String arguments = format( + "--into %s%n" + + "--nodes %s --relationships %s", + dbRule.getStoreDirAbsolutePath(), + nodeData( true, config, nodeIds, TRUE ).getAbsolutePath(), + relationshipData( true, config, nodeIds, TRUE, true ).getAbsolutePath() ); + writeToFile( argumentFile, arguments, false ); + + // when + importTool( "-f", argumentFile.getAbsolutePath() ); + + // then + verifyData(); + } + + @Test + public void shouldFailIfSupplyingBothFileArgumentAndAnyOtherArgument() throws Exception + { + // given + List nodeIds = nodeIds(); + Configuration config = Configuration.COMMAS; + File argumentFile = file( "args" ); + String arguments = format( + "--into %s%n" + + "--nodes %s --relationships %s", + dbRule.getStoreDirAbsolutePath(), + nodeData( true, config, nodeIds, TRUE ).getAbsolutePath(), + relationshipData( true, config, nodeIds, TRUE, true ).getAbsolutePath() ); + writeToFile( argumentFile, arguments, false ); + + try + { + // when + importTool( "-f", argumentFile.getAbsolutePath(), "--into", dbRule.getStoreDirAbsolutePath() ); + fail( "Should have failed" ); + } + catch ( IllegalArgumentException e ) + { + // then good + assertThat( e.getMessage(), containsString( "in addition to" ) ); + assertThat( e.getMessage(), containsString( ImportTool.Options.FILE.argument() ) ); + } + } + private void assertContains( List errorLines, String string ) { for ( String line : errorLines ) diff --git a/community/shell/src/main/java/org/neo4j/shell/AppCommandParser.java b/community/shell/src/main/java/org/neo4j/shell/AppCommandParser.java index a8a800afb410..bc3ae070de33 100644 --- a/community/shell/src/main/java/org/neo4j/shell/AppCommandParser.java +++ b/community/shell/src/main/java/org/neo4j/shell/AppCommandParser.java @@ -26,7 +26,7 @@ import org.neo4j.shell.apps.NoopApp; -import static org.neo4j.shell.TextUtil.tokenizeStringWithQuotes; +import static org.neo4j.helpers.TextUtil.tokenizeStringWithQuotes; /** * Parses a line from the client with the intention of interpreting it as @@ -126,7 +126,7 @@ private void parseApp( String line ) throws Exception private void parseParameters( String line ) throws ShellException { String rest = line.substring( appName.length() ).trim(); - String[] parsed = tokenizeStringWithQuotes( rest, false ); + String[] parsed = tokenizeStringWithQuotes( rest, false, false ); for ( int i = 0; i < parsed.length; i++ ) { String string = parsed[i]; diff --git a/community/shell/src/main/java/org/neo4j/shell/apps/Export.java b/community/shell/src/main/java/org/neo4j/shell/apps/Export.java index fc2d164f1031..9492805c6d29 100644 --- a/community/shell/src/main/java/org/neo4j/shell/apps/Export.java +++ b/community/shell/src/main/java/org/neo4j/shell/apps/Export.java @@ -30,8 +30,6 @@ import org.neo4j.shell.impl.AbstractApp; import org.neo4j.shell.util.json.JSONParser; -import static org.neo4j.shell.TextUtil.stripFromQuotes; - /** * Mimics the Bash application "export" and uses the client session * {@link Session} as the data container. @@ -91,4 +89,16 @@ private Object stripFromQuotesIfString( Object value ) { return value instanceof String ? stripFromQuotes( value.toString() ) : value; } + + private static String stripFromQuotes( String string ) + { + if ( string != null ) + { + if ( string.startsWith( "\"" ) && string.endsWith( "\"" ) ) + { + return string.substring( 1, string.length() - 1 ); + } + } + return string; + } } diff --git a/community/shell/src/main/java/org/neo4j/shell/apps/extra/ScriptExecutor.java b/community/shell/src/main/java/org/neo4j/shell/apps/extra/ScriptExecutor.java index 5a828516351a..9f396a761b3a 100644 --- a/community/shell/src/main/java/org/neo4j/shell/apps/extra/ScriptExecutor.java +++ b/community/shell/src/main/java/org/neo4j/shell/apps/extra/ScriptExecutor.java @@ -31,7 +31,7 @@ import org.neo4j.shell.Session; import org.neo4j.shell.ShellException; -import static org.neo4j.shell.TextUtil.tokenizeStringWithQuotes; +import static org.neo4j.helpers.TextUtil.tokenizeStringWithQuotes; /** * Executes groovy scripts purely via reflection diff --git a/community/shell/src/main/java/org/neo4j/shell/impl/AbstractAppServer.java b/community/shell/src/main/java/org/neo4j/shell/impl/AbstractAppServer.java index d7405156fc1f..d97c6bb17b8b 100644 --- a/community/shell/src/main/java/org/neo4j/shell/impl/AbstractAppServer.java +++ b/community/shell/src/main/java/org/neo4j/shell/impl/AbstractAppServer.java @@ -30,6 +30,7 @@ import java.util.regex.Pattern; import org.neo4j.helpers.Service; +import org.neo4j.helpers.TextUtil; import org.neo4j.shell.App; import org.neo4j.shell.AppCommandParser; import org.neo4j.shell.AppShellServer; @@ -39,7 +40,6 @@ import org.neo4j.shell.Session; import org.neo4j.shell.ShellException; import org.neo4j.shell.TabCompletion; -import org.neo4j.shell.TextUtil; /** * A common implementation of an {@link AppShellServer}. The server can be given @@ -120,7 +120,7 @@ public Response interpretLine( Serializable clientId, String line, Output out ) Continuation commandResult = null; for ( String command : line.split( Pattern.quote( "&&" ) ) ) { - command = TextUtil.removeSpaces( command ); + command = removeSpaces( command ); command = replaceAlias( command, session ); AppCommandParser parser = new AppCommandParser( this, command ); commandResult = parser.app().execute( parser, session, out ); @@ -133,6 +133,19 @@ public Response interpretLine( Serializable clientId, String line, Output out ) } } + private static String removeSpaces( String command ) + { + while ( command.length() > 0 && command.charAt( 0 ) == ' ' ) + { + command = command.substring( 1 ); + } + while ( command.length() > 0 && command.charAt( command.length() - 1 ) == ' ' ) + { + command = command.substring( 0, command.length() - 1 ); + } + return command; + } + protected String replaceAlias( String line, Session session ) { boolean changed = true; diff --git a/community/shell/src/main/java/org/neo4j/shell/kernel/apps/Cd.java b/community/shell/src/main/java/org/neo4j/shell/kernel/apps/Cd.java index 3db7b0e884ad..796de8abdfa9 100644 --- a/community/shell/src/main/java/org/neo4j/shell/kernel/apps/Cd.java +++ b/community/shell/src/main/java/org/neo4j/shell/kernel/apps/Cd.java @@ -43,7 +43,7 @@ import org.neo4j.shell.ShellException; import org.neo4j.shell.impl.RelationshipToNodeIterable; -import static org.neo4j.shell.TextUtil.lastWordOrQuoteOf; +import static org.neo4j.helpers.TextUtil.lastWordOrQuoteOf; /** * Mimics the POSIX application with the same name, i.e. traverses to a node. diff --git a/community/shell/src/main/java/org/neo4j/shell/kernel/apps/TransactionProvidingApp.java b/community/shell/src/main/java/org/neo4j/shell/kernel/apps/TransactionProvidingApp.java index 7691dd56d518..84ecc3af1eb6 100644 --- a/community/shell/src/main/java/org/neo4j/shell/kernel/apps/TransactionProvidingApp.java +++ b/community/shell/src/main/java/org/neo4j/shell/kernel/apps/TransactionProvidingApp.java @@ -43,6 +43,7 @@ import org.neo4j.graphdb.Relationship; import org.neo4j.graphdb.RelationshipType; import org.neo4j.graphdb.Transaction; +import org.neo4j.helpers.TextUtil; import org.neo4j.helpers.collection.Pair; import org.neo4j.shell.App; import org.neo4j.shell.AppCommandParser; @@ -52,7 +53,6 @@ import org.neo4j.shell.Output; import org.neo4j.shell.Session; import org.neo4j.shell.ShellException; -import org.neo4j.shell.TextUtil; import org.neo4j.shell.impl.AbstractApp; import org.neo4j.shell.kernel.GraphDatabaseShellServer; import org.neo4j.shell.util.json.JSONArray;