Skip to content

Commit

Permalink
Begin HOCON support
Browse files Browse the repository at this point in the history
  • Loading branch information
TheElectronWill committed Dec 26, 2016
1 parent 28c948f commit 9271aeb
Show file tree
Hide file tree
Showing 5 changed files with 344 additions and 0 deletions.
13 changes: 13 additions & 0 deletions hocon/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
version 'unspecified'

apply plugin: 'java'

sourceCompatibility = 1.5

repositories {
mavenCentral()
}

dependencies {
testCompile group: 'junit', name: 'junit', version: '4.11'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.electronwill.nightconfig.hocon;

import com.electronwill.nightconfig.core.Config;
import com.electronwill.nightconfig.core.MapConfig;
import com.electronwill.nightconfig.core.serialization.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.List;

/**
* @author TheElectronWill
*/
public final class HoconConfig extends MapConfig implements FileConfig {

private static final HashSet<Class<?>> SUPPORTED_TYPES = new HashSet<>();

static {
SUPPORTED_TYPES.add(Integer.class);
SUPPORTED_TYPES.add(Long.class);
SUPPORTED_TYPES.add(Float.class);
SUPPORTED_TYPES.add(Double.class);
SUPPORTED_TYPES.add(Boolean.class);
SUPPORTED_TYPES.add(String.class);
SUPPORTED_TYPES.add(List.class);
SUPPORTED_TYPES.add(Config.class);
}

@Override
public boolean supportsType(Class<?> type) {
return SUPPORTED_TYPES.contains(type) || List.class.isAssignableFrom(type) || Config.class.isAssignableFrom(type);
}

@Override
public HoconConfig createEmptyConfig() {
return new HoconConfig();
}

@Override
public void writeTo(File file) throws IOException {
try (Writer fileWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
CharacterOutput output = new WriterOutput(fileWriter);
HoconWriter jsonWriter = new HoconWriter(output);
jsonWriter.writeJsonObject(this);
}//finally closes the writer
}

@Override
public void readFrom(File file) throws IOException {
try (Reader fileReader = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
CharacterInput input = new ReaderInput(fileReader);
HoconParser jsonParser = new HoconParser(input);
this.asMap().clear();//clears the config
jsonParser.parseJsonObject(this);//reads the value from the file to the config
}//finally closes the reader
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package com.electronwill.nightconfig.hocon;

import com.electronwill.nightconfig.core.serialization.CharacterInput;
import com.electronwill.nightconfig.core.serialization.CharsWrapper;
import com.electronwill.nightconfig.core.serialization.ParsingException;
import com.electronwill.nightconfig.core.serialization.Utils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
* @author TheElectronWill
*/
public final class HoconParser {
private static final char[] SPACES = {' ', '\t', '\n', '\r'};
private static final char[] TRUE_LAST = {'r', 'u', 'e'}, FALSE_LAST = {'a', 'l', 's', 'e'};
private static final char[] NULL_LAST = {'u', 'l', 'l'};
private static final char[] NUMBER_END = {',', '}', ']', ' ', '\t', '\n', '\r'};

private final CharacterInput input;

public HoconParser(CharacterInput input) {
this.input = input;
}

public HoconConfig parseHoconObject() {
HoconConfig config = new HoconConfig();
parseHoconObject(config);
return config;
}

public void parseHoconObject(HoconConfig config) {
char firstChar = input.readCharAndSkip(SPACES);
if (firstChar != '{')
throw new ParsingException("Invalid first character for a json object: " + firstChar);
parseObject(config);
}

private HoconConfig parseObject(HoconConfig config) {
while (true) {
char keyFirst = input.readCharAndSkip(SPACES);
if (keyFirst != '"')
throw new ParsingException("Invalid beginning of a key: " + keyFirst);

String key = parseString();
char separator = input.readCharAndSkip(SPACES);
if (separator != ':')
throw new ParsingException("Invalid key/value separator: " + separator);

char valueFirst = input.readCharAndSkip(SPACES);
Object value = parseValue(valueFirst);
config.setValue(key, value);

char next = input.readCharAndSkip(SPACES);
if (next == '}')//end of the object
return config;
else if (next != ',')
throw new ParsingException("Invalid value separator: " + next);
}
}

private List<Object> parseArray() {
final List<Object> list = new ArrayList<>();
while (true) {
char valueFirst = input.readCharAndSkip(SPACES);
Object value = parseValue(valueFirst);
list.add(value);

char next = input.readCharAndSkip(SPACES);
if (next == ']')//end of the array
return list;
else if (next != ',')//invalid separator
throw new ParsingException("Invalid value separator: " + valueFirst);
}
}

private Object parseValue(char firstChar) {
switch (firstChar) {
case '"':
return parseString();
case '{':
return parseObject(new HoconConfig());
case '[':
return parseArray();
case 't':
return parseTrue();
case 'f':
return parseFalse();
case 'n':
return parseNull();
default:
return parseNumber();
}
}

private Number parseNumber() {
CharsWrapper chars = input.readCharUntil(NUMBER_END);
if (chars.contains('.') || chars.contains('e') || chars.contains('E')) {//must be a double
return Utils.parseDouble(chars);
}
return Utils.parseLong(chars, 10);
}

private boolean parseTrue() {
char[] chars = input.readChars(3);
if (!Arrays.equals(chars, TRUE_LAST))
throw new ParsingException("Invalid value: t" + new CharsWrapper(chars) + " - expected boolean true");
return true;
}

private boolean parseFalse() {
char[] chars = input.readChars(4);
if (!Arrays.equals(chars, FALSE_LAST))
throw new ParsingException("Invalid value: f" + new CharsWrapper(chars) + " - expected boolean false");
return false;
}

private Object parseNull() {
char[] chars = input.readChars(3);
if (!Arrays.equals(chars, NULL_LAST))
throw new ParsingException("Invaid value: n" + new CharsWrapper(chars) + " - expected null");
return null;
}

private String parseString() {
StringBuilder builder = new StringBuilder();
boolean escape = false;
for (char c = input.readChar(); c != '"' || escape; c = input.readChar()) {
if (escape) {
builder.append(escape(c));
escape = false;
} else if (c == '\\') {
escape = true;
} else {
builder.append(c);
}
}
return builder.toString();
}

private char escape(char c) {
switch (c) {
case '"':
case '\\':
case '/':
return c;
case 'b':
return '\b';
case 'f':
return '\f';
case 'n':
return '\n';
case 'r':
return '\r';
case 't':
return '\t';
case 'u':
char[] chars = input.readChars(4);
return (char)Utils.parseInt(chars, 16);
default:
throw new ParsingException("Invalid escapement: \\" + c);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.electronwill.nightconfig.hocon;

import com.electronwill.nightconfig.core.Config;
import com.electronwill.nightconfig.core.serialization.CharacterOutput;
import com.electronwill.nightconfig.core.serialization.SerializationException;
import com.electronwill.nightconfig.core.serialization.Utils;
import java.util.Iterator;
import java.util.Map;

/**
* @author TheElectronWill
*/
public final class HoconWriter {
static final char[] NULL_CHARS = {'n', 'u', 'l', 'l'};
static final char[] TRUE_CHARS = {'t', 'r', 'u', 'e'};
static final char[] FALSE_CHARS = {'f', 'a', 'l', 's', 'e'};
static final char[] TO_ESCAPE = {'"', '\n', '\r', '\t', '\\'};
static final char[] ESCAPED = {'"', 'n', 'r', 't', '\\'};

private final CharacterOutput output;

public HoconWriter(CharacterOutput output) {
this.output = output;
}

public void writeJsonObject(Config config) {
writeObject(config);
}

private void writeObject(Config config) {
output.write('{');
final Iterator<Map.Entry<String, Object>> it = config.asMap().entrySet().iterator();
do {
final Map.Entry<String, Object> entry = it.next();
final String key = entry.getKey();
final Object value = entry.getValue();
writeString(key);
output.write(':');
writeValue(value);
if (it.hasNext())
output.write(',');
else
break;

} while (true);
output.write('}');
}

private void writeValue(Object v) {
if (v == null)
writeNull();
else if (v instanceof CharSequence)
writeString((CharSequence)v);
else if (v instanceof Number)
output.write(v.toString());
else if (v instanceof Config)
writeObject((Config)v);
else if (v instanceof Iterable)
writeArray((Iterable)v);
else if (v instanceof Boolean)
writeBoolean((boolean)v);
else
throw new SerializationException("Unsupported value type: " + v.getClass());
}

private void writeArray(Iterable<?> iterable) {
output.write('[');
final Iterator<?> it = iterable.iterator();
do {
Object value = it.next();
writeValue(value);
if (it.hasNext())
output.write(',');
else
break;
} while (true);
output.write(']');
}

private void writeBoolean(boolean b) {
if (b)
output.write(TRUE_CHARS);
else
output.write(FALSE_CHARS);
}

private void writeNull() {
output.write(NULL_CHARS);
}

private void writeString(CharSequence s) {
output.write('"');
final int length = s.length();
for (int i = 0; i < length; i++) {
char c = s.charAt(i);
int escapeIndex = Utils.arrayIndexOf(TO_ESCAPE, c);
if (escapeIndex != -1) {
char escaped = ESCAPED[escapeIndex];
output.write('\\');
output.write(escaped);
} else {
output.write(c);
}
}
output.write('"');
}

}
2 changes: 2 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ rootProject.name = 'Night Config'
include 'core'
include 'json'
include 'toml'
include 'hocon'

0 comments on commit 9271aeb

Please sign in to comment.