diff --git a/aliyun-java-sdk-core/pom.xml b/aliyun-java-sdk-core/pom.xml index e93f61e16d..73467634da 100644 --- a/aliyun-java-sdk-core/pom.xml +++ b/aliyun-java-sdk-core/pom.xml @@ -140,6 +140,7 @@ org.ini4j ini4j 0.5.4 + test org.slf4j diff --git a/aliyun-java-sdk-core/src/main/java/com/aliyuncs/auth/ProfileCredentialsProvider.java b/aliyun-java-sdk-core/src/main/java/com/aliyuncs/auth/ProfileCredentialsProvider.java index de0601efc7..c32398046e 100644 --- a/aliyun-java-sdk-core/src/main/java/com/aliyuncs/auth/ProfileCredentialsProvider.java +++ b/aliyun-java-sdk-core/src/main/java/com/aliyuncs/auth/ProfileCredentialsProvider.java @@ -2,23 +2,20 @@ import com.aliyuncs.exceptions.ClientException; import com.aliyuncs.utils.AuthUtils; +import com.aliyuncs.utils.ProfileUtils; import com.aliyuncs.utils.StringUtils; -import org.ini4j.Profile; -import org.ini4j.Wini; - -import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.Map; public class ProfileCredentialsProvider implements AlibabaCloudCredentialsProvider { - private static volatile Wini ini; + private static volatile Map> ini; - private static Wini getIni(String filePath) throws IOException { + private static Map> getIni(String filePath) throws IOException { if (null == ini) { synchronized (ProfileCredentialsProvider.class) { if (null == ini) { - ini = new Wini(new File(filePath)); + ini = ProfileUtils.parseFile(filePath); } } } @@ -34,7 +31,7 @@ public AlibabaCloudCredentials getCredentials() throws ClientException { if (filePath.isEmpty()) { throw new ClientException("The specified credentials file is empty"); } - Wini ini; + Map> ini; try { ini = getIni(filePath); } catch (IOException e) { @@ -49,16 +46,14 @@ public AlibabaCloudCredentials getCredentials() throws ClientException { return createCredential(clientConfig, credentialsProviderFactory); } - private Map> loadIni(Wini ini) { + private Map> loadIni(Map> ini) { Map> client = new HashMap>(); - boolean enable; - for (Map.Entry clientType : ini.entrySet()) { - enable = clientType.getValue().get(AuthConstant.INI_ENABLE, boolean.class); - if (enable) { + String enable; + for (Map.Entry> clientType : ini.entrySet()) { + enable = clientType.getValue().get(AuthConstant.INI_ENABLE); + if (Boolean.parseBoolean(enable)) { Map clientConfig = new HashMap(); - for (Map.Entry enabledClient : clientType.getValue().entrySet()) { - clientConfig.put(enabledClient.getKey(), enabledClient.getValue()); - } + clientConfig.putAll(clientType.getValue()); client.put(clientType.getKey(), clientConfig); } } diff --git a/aliyun-java-sdk-core/src/main/java/com/aliyuncs/utils/ProfileUtils.java b/aliyun-java-sdk-core/src/main/java/com/aliyuncs/utils/ProfileUtils.java new file mode 100644 index 0000000000..41eecbf092 --- /dev/null +++ b/aliyun-java-sdk-core/src/main/java/com/aliyuncs/utils/ProfileUtils.java @@ -0,0 +1,194 @@ +package com.aliyuncs.utils; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.io.*; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * Only for internal use within the package, do not use it arbitrarily, backward compatibility and sustainability cannot be guaranteed. + */ +public class ProfileUtils { + private final static Log log = LogFactory.getLog(ProfileUtils.class); + private static final Pattern EMPTY_LINE = Pattern.compile("^[\t ]*$"); + + public static Map> parseFile(String profilePath) throws IOException { + return parseFile(new FileReader(profilePath)); + } + + static Map> parseFile(Reader input) throws IOException { + ParserProgress progress = new ParserProgress(); + BufferedReader profileReader = null; + try { + profileReader = new BufferedReader(input); + String line; + while ((line = profileReader.readLine()) != null) { + parseLine(progress, line); + } + } finally { + if (profileReader != null) { + profileReader.close(); + } + } + return progress.profiles; + } + + private static void parseLine(ParserProgress progress, String line) { + ++progress.currentLineNumber; + if (!EMPTY_LINE.matcher(line).matches() && !(line.startsWith("#") || line.startsWith(";"))) { + if (isSectionDefinitionLine(line)) { + readSectionDefinitionLine(progress, line); + } else if (line.startsWith("\t")) { + readPropertyContinuationLine(progress, line); + } else { + readPropertyDefinitionLine(progress, line); + } + } + } + + private static void readSectionDefinitionLine(ParserProgress progress, String line) { + String lineWithoutComments = removeTrailingComments(line, "#", ";"); + String lineWithoutWhitespace = lineWithoutComments.trim(); + + if (!lineWithoutWhitespace.endsWith("]")) { + throw new IllegalArgumentException(String.format("Section definition must end with ']' on line %s: %s", progress.currentLineNumber, line)); + } + + String lineWithoutBrackets = lineWithoutWhitespace.substring(1, lineWithoutWhitespace.length() - 1); + String profileName = lineWithoutBrackets.trim(); + if (profileName.isEmpty()) { + progress.ignoringCurrentProfile = true; + return; + } + progress.currentProfileBeingRead = profileName; + progress.currentPropertyBeingRead = null; + progress.ignoringCurrentProfile = false; + if (!progress.profiles.containsKey(profileName)) { + progress.profiles.put(profileName, new LinkedHashMap()); + } + } + + private static void readPropertyDefinitionLine(ParserProgress progress, String line) { + // Invalid profile, ignore its properties + if (progress.ignoringCurrentProfile) { + return; + } + if (progress.currentProfileBeingRead == null) { + // throw new IllegalArgumentException(String.format("Expected a profile definition on line %s", progress.currentLineNumber)); + // To be consistent with ini4j's behavior + progress.currentProfileBeingRead = "?"; + if (!progress.profiles.containsKey(progress.currentProfileBeingRead)) { + progress.profiles.put(progress.currentProfileBeingRead, new LinkedHashMap()); + } + } + + // Comments with property must have whitespace before them, or they will be considered part of the value + String lineWithoutComments = removeTrailingComments(line, " #", " ;", "\t#", "\t;"); + String lineWithoutWhitespace = lineWithoutComments.trim(); + Property property = parsePropertyDefinition(progress, lineWithoutWhitespace); + + if (progress.profiles.get(progress.currentProfileBeingRead).containsKey(property.key())) { + log.warn("Duplicate property '" + property.key() + "' detected on line " + progress.currentLineNumber + + ". The later one in the file will be used."); + } + + progress.currentPropertyBeingRead = property.key(); + + progress.profiles.get(progress.currentProfileBeingRead).put(property.key(), property.value()); + } + + private static void readPropertyContinuationLine(ParserProgress progress, String line) { + // Invalid profile, ignore its properties + if (progress.ignoringCurrentProfile) { + return; + } + if (progress.currentProfileBeingRead == null) { + // throw new IllegalArgumentException(String.format("Expected a profile definition on line %s", progress.currentLineNumber)); + // To be consistent with ini4j's behavior + progress.currentProfileBeingRead = "?"; + if (!progress.profiles.containsKey(progress.currentProfileBeingRead)) { + progress.profiles.put(progress.currentProfileBeingRead, new LinkedHashMap()); + } + } + + // Comments are not removed on property continuation lines. They're considered part of the value. + line = line.trim(); + Map profileProperties = progress.profiles.get(progress.currentProfileBeingRead); + + String currentPropertyValue = profileProperties.get(progress.currentPropertyBeingRead); + String newPropertyValue = currentPropertyValue + "\n" + line; + profileProperties.put(progress.currentPropertyBeingRead, newPropertyValue); + } + + private static Property parsePropertyDefinition(ParserProgress progress, String line) { + int firstEqualsLocation = line.indexOf('='); + String propertyKey = null; + String propertyValue = null; + if (firstEqualsLocation == -1) { + // throw new IllegalArgumentException(String.format("Expected an '=' sign defining a property on line %s", progress.currentLineNumber)); + // To be consistent with ini4j's behavior + propertyKey = line.trim(); + } else { + propertyKey = line.substring(0, firstEqualsLocation).trim(); + propertyValue = line.substring(firstEqualsLocation + 1).trim(); + } + + if (propertyKey.isEmpty()) { + throw new IllegalArgumentException(String.format("Property did not have a name on line %s", progress.currentLineNumber)); + } + + return new Property(propertyKey, propertyValue); + } + + private static boolean isSectionDefinitionLine(String line) { + return line.trim().startsWith("["); + } + + private static String removeTrailingComments(String line, String... commentPatterns) { + int earliestMatchIndex = line.length(); + for (String pattern : commentPatterns) { + int index = line.indexOf(pattern); + if (index >= 0 && index < earliestMatchIndex) { + earliestMatchIndex = index; + } + } + return line.substring(0, earliestMatchIndex); + } + + private static final class ParserProgress { + private int currentLineNumber; + private String currentProfileBeingRead; + private String currentPropertyBeingRead; + private boolean ignoringCurrentProfile; + private final Map> profiles; + + private ParserProgress() { + this.currentLineNumber = 0; + this.currentProfileBeingRead = null; + this.currentPropertyBeingRead = null; + this.ignoringCurrentProfile = false; + this.profiles = new LinkedHashMap>(); + } + } + + private static final class Property { + private final Key key; + private final Value value; + + private Property(Key key, Value value) { + this.key = key; + this.value = value; + } + + public Key key() { + return this.key; + } + + public Value value() { + return this.value; + } + } +} diff --git a/aliyun-java-sdk-core/src/test/java/com/aliyuncs/auth/ProfileCredentialsProviderTest.java b/aliyun-java-sdk-core/src/test/java/com/aliyuncs/auth/ProfileCredentialsProviderTest.java index a269ff085f..81d747b65a 100644 --- a/aliyun-java-sdk-core/src/test/java/com/aliyuncs/auth/ProfileCredentialsProviderTest.java +++ b/aliyun-java-sdk-core/src/test/java/com/aliyuncs/auth/ProfileCredentialsProviderTest.java @@ -2,7 +2,6 @@ import com.aliyuncs.exceptions.ClientException; import com.aliyuncs.utils.AuthUtils; -import org.ini4j.Wini; import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; @@ -200,8 +199,8 @@ public void getIniTest() throws NoSuchMethodException, InvocationTargetException getIni.setAccessible(true); String file = ProfileCredentialsProviderTest.class.getClassLoader(). getResource("configTest.ini").getPath(); - Wini firstIni = (Wini) getIni.invoke(profileCredentialsProvider, file); - Wini secondIni = (Wini) getIni.invoke(profileCredentialsProvider, file); + Map> firstIni = (Map>) getIni.invoke(profileCredentialsProvider, file); + Map> secondIni = (Map>) getIni.invoke(profileCredentialsProvider, file); Assert.assertTrue(firstIni.equals(secondIni)); } } diff --git a/aliyun-java-sdk-core/src/test/java/com/aliyuncs/utils/ProfileUtilsTest.java b/aliyun-java-sdk-core/src/test/java/com/aliyuncs/utils/ProfileUtilsTest.java new file mode 100644 index 0000000000..be1cdd44ee --- /dev/null +++ b/aliyun-java-sdk-core/src/test/java/com/aliyuncs/utils/ProfileUtilsTest.java @@ -0,0 +1,162 @@ +package com.aliyuncs.utils; + +import org.ini4j.Wini; +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.io.StringReader; +import java.util.Map; + +public class ProfileUtilsTest { + @Test + public void testProfile() throws IOException { + new ProfileUtils(); + String context = "[profile1]\n" + + ";comment\n" + + "#comment\n" + + "enable = false #comment\n" + + "[profile2]\n" + + "region = cn-hangzhou#comment\n" + + "[default]\n" + + "default_property = property1 \t\n\n" + + "[default]\n" + + "default_property = property2\n" + + "[profile3]\n" + + "int = 1\n" + + " int = 2\n" + + "int = \n" + + "\t3\n" + + "str = #comment\n" + + "\ttest\n"; + Wini ini = new Wini(new StringReader(context)); + Map> iniMap = ProfileUtils.parseFile(new StringReader(context)); + Assert.assertEquals(4, ini.size()); + Assert.assertEquals(4, iniMap.size()); + Assert.assertEquals("false #comment", ini.get("profile1").get("enable")); + Assert.assertEquals("false", iniMap.get("profile1").get("enable")); + Assert.assertEquals(ini.get("profile2").get("region"), iniMap.get("profile2").get("region")); + Assert.assertEquals(ini.get("default").get("default_property"), iniMap.get("default").get("default_property")); + Assert.assertEquals("\n3", iniMap.get("profile3").get("int")); + Assert.assertEquals("\ntest", iniMap.get("profile3").get("str")); + + context = "[profile1]\n" + + "enable = false\n" + + "[profile2]\n" + + "enable = true\n" + + "[profile3]\n" + + "enable = null\n" + + "[profile4]\n" + + "enable = 1\n" + + "[profile5]\n" + + "enable = False\n" + + "[profile6]\n" + + "enable = True\n" + + "[profile7]\n" + + "enable =\n"; + ini = new Wini(new StringReader(context)); + iniMap = ProfileUtils.parseFile(new StringReader(context)); + Assert.assertEquals(7, ini.size()); + Assert.assertEquals(7, iniMap.size()); + Assert.assertEquals(false, ini.get("profile1").get("enable", Boolean.class)); + Assert.assertEquals(false, Boolean.parseBoolean(iniMap.get("profile1").get("enable"))); + Assert.assertEquals(true, ini.get("profile2").get("enable", Boolean.class)); + Assert.assertEquals(true, Boolean.parseBoolean(iniMap.get("profile2").get("enable"))); + Assert.assertEquals(false, ini.get("profile3").get("enable", Boolean.class)); + Assert.assertEquals(false, Boolean.parseBoolean(iniMap.get("profile3").get("enable"))); + Assert.assertEquals(false, ini.get("profile4").get("enable", Boolean.class)); + Assert.assertEquals(false, Boolean.parseBoolean(iniMap.get("profile4").get("enable"))); + Assert.assertEquals(false, ini.get("profile5").get("enable", Boolean.class)); + Assert.assertEquals(false, Boolean.parseBoolean(iniMap.get("profile5").get("enable"))); + Assert.assertEquals(true, ini.get("profile6").get("enable", Boolean.class)); + Assert.assertEquals(true, Boolean.parseBoolean(iniMap.get("profile6").get("enable"))); + Assert.assertEquals(false, ini.get("profile7").get("enable", Boolean.class)); + Assert.assertEquals(false, Boolean.parseBoolean(iniMap.get("profile7").get("enable"))); + + context = "[invalid\n" + + "enable = false\n"; + try { + new Wini(new StringReader(context)); + Assert.fail(); + } catch (Exception e) { + Assert.assertTrue(e.getMessage().contains("parse error")); + } + try { + ProfileUtils.parseFile(new StringReader(context)); + Assert.fail(); + } catch (Exception e) { + Assert.assertEquals("Section definition must end with ']' on line 1: [invalid", e.getMessage()); + } + + context = "[ ]\n" + + "enable = false\n" + + "str = \n" + + "\ttest\n"; + ; + try { + new Wini(new StringReader(context)); + Assert.fail(); + } catch (Exception e) { + Assert.assertTrue(e.getMessage().contains("parse error")); + } + iniMap = ProfileUtils.parseFile(new StringReader(context)); + Assert.assertEquals(0, iniMap.size()); + + context = "[profile1]\n" + + "[profile2]\n"; + ini = new Wini(new StringReader(context)); + iniMap = ProfileUtils.parseFile(new StringReader(context)); + Assert.assertEquals(2, ini.size()); + Assert.assertEquals(2, iniMap.size()); + Assert.assertEquals(ini.get("profile1").size(), iniMap.get("profile1").size()); + Assert.assertEquals(ini.get("profile2").size(), iniMap.get("profile2").size()); + + context = "enable=true\n" + + "key=value\n" + + "str = \n" + + "\ttest\n"; + ini = new Wini(new StringReader(context)); + iniMap = ProfileUtils.parseFile(new StringReader(context)); + Assert.assertEquals(1, ini.size()); + Assert.assertEquals(1, iniMap.size()); + Assert.assertEquals(4, ini.get("?").size()); + Assert.assertEquals(3, iniMap.get("?").size()); + Assert.assertEquals("true", ini.get("?").get("enable")); + Assert.assertEquals("true", iniMap.get("?").get("enable")); + Assert.assertEquals("value", ini.get("?").get("key")); + Assert.assertEquals("value", iniMap.get("?").get("key")); + + context = "\ttest\n"; + ini = new Wini(new StringReader(context)); + iniMap = ProfileUtils.parseFile(new StringReader(context)); + Assert.assertEquals(1, ini.size()); + Assert.assertEquals(1, iniMap.size()); + Assert.assertEquals(1, ini.get("?").size()); + Assert.assertEquals(1, iniMap.get("?").size()); + + context = "[profile1]\n" + + "key \n"; + ini = new Wini(new StringReader(context)); + iniMap = ProfileUtils.parseFile(new StringReader(context)); + Assert.assertEquals(1, ini.size()); + Assert.assertEquals(1, iniMap.size()); + Assert.assertNull(ini.get("profile1").get("key")); + Assert.assertNull(iniMap.get("profile1").get("key")); + + context = "[profile1]\n" + + " = value\n"; + try { + new Wini(new StringReader(context)); + Assert.fail(); + } catch (Exception e) { + Assert.assertTrue(e.getMessage().contains("parse error")); + } + try { + ProfileUtils.parseFile(new StringReader(context)); + Assert.fail(); + } catch (Exception e) { + Assert.assertEquals("Property did not have a name on line 2", e.getMessage()); + } + } + +} diff --git a/aliyun-java-sdk-core/src/test/resources/configTest.ini b/aliyun-java-sdk-core/src/test/resources/configTest.ini index de3379e6cd..cfcbacbc96 100644 --- a/aliyun-java-sdk-core/src/test/resources/configTest.ini +++ b/aliyun-java-sdk-core/src/test/resources/configTest.ini @@ -1,4 +1,18 @@ -[default] +[ ] ;invalid +enable = false ;comments +enable = true #comments +type = access_key +access_key_id = foo + +[client] ;comments +enable = false ;comments +enable = true #comments +type = access_key # type +access_key_id = foo # access_key_id +access_key_secret = bar # access_key_secret + + +[default] enable = true type = access_key access_key_id = foo