Skip to content

Commit

Permalink
refactor: improve ini config file parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
yndu13 committed Sep 27, 2024
1 parent 7ed407e commit 2ec8d75
Show file tree
Hide file tree
Showing 6 changed files with 386 additions and 16 deletions.
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
<groupId>org.ini4j</groupId>
<artifactId>ini4j</artifactId>
<version>0.5.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,21 @@

import com.aliyun.credentials.exception.CredentialException;
import com.aliyun.credentials.models.CredentialModel;
import com.aliyun.credentials.utils.ProfileUtils;
import com.aliyun.credentials.utils.AuthConstant;
import com.aliyun.credentials.utils.AuthUtils;
import com.aliyun.credentials.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<String, Map<String, String>> ini;

private static Wini getIni(String filePath) throws IOException {
private static Map<String, Map<String, String>> getIni(String filePath) throws IOException {
if (null == ini) {
ini = new Wini(new File(filePath));
ini = ProfileUtils.parseFile(filePath);
}
return ini;
}
Expand All @@ -32,11 +30,11 @@ public CredentialModel getCredentials() {
if (filePath.length() == 0) {
throw new CredentialException("The specified credentials file is empty.");
}
Wini ini;
Map<String, Map<String, String>> ini;
try {
ini = getIni(filePath);
} catch (IOException e) {
throw new CredentialException(String.format("Unable to open credentials file: %s.",filePath));
throw new CredentialException(String.format("Unable to open credentials file: %s.", filePath));
}
Map<String, Map<String, String>> client = loadIni(ini);
Map<String, String> clientConfig = client.get(AuthUtils.getClientType());
Expand All @@ -47,12 +45,12 @@ public CredentialModel getCredentials() {
return createCredential(clientConfig, credentialsProviderFactory);
}

private Map<String, Map<String, String>> loadIni(Wini ini) {
private Map<String, Map<String, String>> loadIni(Map<String, Map<String, String>> ini) {
Map<String, Map<String, String>> client = new HashMap<String, Map<String, String>>(16);
boolean enable;
for (Map.Entry<String, Profile.Section> clientType : ini.entrySet()) {
enable = clientType.getValue().get(AuthConstant.INI_ENABLE, boolean.class);
if (enable) {
String enable;
for (Map.Entry<String, Map<String, String>> clientType : ini.entrySet()) {
enable = clientType.getValue().get(AuthConstant.INI_ENABLE);
if (Boolean.parseBoolean(enable)) {
Map<String, String> clientConfig = new HashMap<String, String>(16);
for (Map.Entry<String, String> enabledClient : clientType.getValue().entrySet()) {
clientConfig.put(enabledClient.getKey(), enabledClient.getValue());
Expand Down
196 changes: 196 additions & 0 deletions src/main/java/com/aliyun/credentials/utils/ProfileUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package com.aliyun.credentials.utils;

import com.aliyun.tea.logging.ClientLogger;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
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 static final ClientLogger logger = new ClientLogger(ProfileUtils.class);
private static final Pattern EMPTY_LINE = Pattern.compile("^[\t ]*$");

public static Map<String, Map<String, String>> parseFile(String profilePath) throws IOException {
return parseFile(new FileReader(profilePath));
}

static Map<String, Map<String, String>> 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<String, String>());
}
}

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<String, String>());
}
}

// 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<String, String> property = parsePropertyDefinition(progress, lineWithoutWhitespace);

if (progress.profiles.get(progress.currentProfileBeingRead).containsKey(property.key())) {
logger.warning("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<String, String>());
}
}

// Comments are not removed on property continuation lines. They're considered part of the value.
line = line.trim();
Map<String, String> 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<String, String> 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<String, String>(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<String, Map<String, String>> profiles;

private ParserProgress() {
this.currentLineNumber = 0;
this.currentProfileBeingRead = null;
this.currentPropertyBeingRead = null;
this.ignoringCurrentProfile = false;
this.profiles = new LinkedHashMap<String, Map<String, String>>();
}
}

private static final class Property<Key, Value> {
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,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<String, Map<String, String>> firstIni = (Map<String, Map<String, String>>) getIni.invoke(profileCredentialsProvider, file);
Map<String, Map<String, String>> secondIni = (Map<String, Map<String, String>>) getIni.invoke(profileCredentialsProvider, file);
Assert.assertTrue(firstIni == secondIni);
}
}
Loading

0 comments on commit 2ec8d75

Please sign in to comment.