/*
* Copyright (C) 2012 Stormpath, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jersey2.simple.lang;
import java.io.InputStream;
import java.util.*;
/**
* Replacement for the java.util.Properties class that retains the order in which the properties
* are defined.
*/
public class OrderPreservingProperties implements Map<String,String> {
public static final String DEFAULT_CHARSET_NAME = "UTF-8";
public static final String COMMENT_POUND = "#";
public static final String COMMENT_SEMICOLON = ";";
protected static final char ESCAPE_TOKEN = '\\';
private final Map<String, String> props;
public OrderPreservingProperties() {
this.props = new LinkedHashMap<String, String>();
}
/**
* Loads the .properties backed by the given InputStream into this instance. This implementation will
* close the input stream after it has finished loading. It is expected that the stream's contents are
* UTF-8 encoded.
*
* @param is the {@code InputStream} from which to read the INI-formatted text
*/
public void load(InputStream is) {
//convert InputStream into a String in one shot:
String string;
try {
string = new Scanner(is, DEFAULT_CHARSET_NAME).useDelimiter("\\A").next();
} catch (NoSuchElementException nsee) {
string = "";
}
Map<String,String> props = toMapProps(string);
putAll(props);
}
//Protected to access in a test case - NOT considered part of Shiro's public API
protected static boolean isContinued(String line) {
if (!StringUtils.hasText(line)) {
return false;
}
int length = line.length();
//find the number of backslashes at the end of the line. If an even number, the
//backslashes are considered escaped. If an odd number, the line is considered continued on the next line
int backslashCount = 0;
for (int i = length - 1; i > 0; i--) {
if (line.charAt(i) == ESCAPE_TOKEN) {
backslashCount++;
} else {
break;
}
}
return backslashCount % 2 != 0;
}
private static boolean isKeyValueSeparatorChar(char c) {
return Character.isWhitespace(c) || c == ':' || c == '=';
}
private static boolean isCharEscaped(CharSequence s, int index) {
return index > 0 && s.charAt(index - 1) == ESCAPE_TOKEN;
}
//Protected to access in a test case - NOT considered part of Shiro's public API
protected static String[] splitKeyValue(String keyValueLine) {
String line = StringUtils.clean(keyValueLine);
if (line == null) {
return null;
}
StringBuilder keyBuffer = new StringBuilder();
StringBuilder valueBuffer = new StringBuilder();
boolean buildingKey = true; //we'll build the value next:
for (int i = 0; i < line.length(); i++) {
char c = line.charAt(i);
if (buildingKey) {
if (isKeyValueSeparatorChar(c) && !isCharEscaped(line, i)) {
buildingKey = false;//now start building the value
} else {
keyBuffer.append(c);
}
} else {
if (valueBuffer.length() == 0 && isKeyValueSeparatorChar(c) && !isCharEscaped(line, i)) {
//swallow the separator chars before we start building the value
} else {
valueBuffer.append(c);
}
}
}
String key = StringUtils.clean(keyBuffer.toString());
String value = StringUtils.clean(valueBuffer.toString());
if (key == null || value == null) {
String msg = "Line argument must contain a key and a value. Only one string token was found.";
throw new IllegalArgumentException(msg);
}
//log.trace("Discovered key/value pair: {}={}", key, value);
return new String[]{key, value};
}
private static Map<String, String> toMapProps(String content) {
Map<String, String> props = new LinkedHashMap<String, String>();
String line;
StringBuilder lineBuffer = new StringBuilder();
Scanner scanner = new Scanner(content);
while (scanner.hasNextLine()) {
line = StringUtils.clean(scanner.nextLine());
if (line == null || line.startsWith(COMMENT_POUND) || line.startsWith(COMMENT_SEMICOLON)) {
//skip empty lines and comments:
continue;
}
if (isContinued(line)) {
//strip off the last continuation backslash:
line = line.substring(0, line.length() - 1);
lineBuffer.append(line);
continue;
} else {
lineBuffer.append(line);
}
line = lineBuffer.toString();
lineBuffer = new StringBuilder();
String[] kvPair = splitKeyValue(line);
props.put(kvPair[0], kvPair[1]);
}
return props;
}
public void clear() {
this.props.clear();
}
public boolean containsKey(Object key) {
return this.props.containsKey(key);
}
public boolean containsValue(Object value) {
return this.props.containsValue(value);
}
public Set<Entry<String, String>> entrySet() {
return this.props.entrySet();
}
public String get(Object key) {
return this.props.get(key);
}
public boolean isEmpty() {
return this.props.isEmpty();
}
public Set<String> keySet() {
return this.props.keySet();
}
public String put(String key, String value) {
return this.props.put(key, value);
}
public void putAll(Map<? extends String, ? extends String> m) {
this.props.putAll(m);
}
public String remove(Object key) {
return this.props.remove(key);
}
public int size() {
return this.props.size();
}
public Collection<String> values() {
return this.props.values();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof OrderPreservingProperties) {
OrderPreservingProperties other = (OrderPreservingProperties) obj;
return this.props.equals(other.props);
}
return false;
}
@Override
public int hashCode() {
return ObjectUtils.nullSafeHashCode(this.props);
}
}