/*******************************************************************************
* Copyright (c) CWI 2009
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Jurgen Vinju (jurgenv@cwi.nl) - initial API and implementation
*******************************************************************************/
package org.rascalmpl.value.io;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import org.rascalmpl.value.IBool;
import org.rascalmpl.value.IConstructor;
import org.rascalmpl.value.IDateTime;
import org.rascalmpl.value.IExternalValue;
import org.rascalmpl.value.IInteger;
import org.rascalmpl.value.IList;
import org.rascalmpl.value.IMap;
import org.rascalmpl.value.INode;
import org.rascalmpl.value.IRational;
import org.rascalmpl.value.IReal;
import org.rascalmpl.value.ISet;
import org.rascalmpl.value.ISourceLocation;
import org.rascalmpl.value.IString;
import org.rascalmpl.value.ITuple;
import org.rascalmpl.value.IValue;
import org.rascalmpl.value.IWithKeywordParameters;
import org.rascalmpl.value.type.ITypeVisitor;
import org.rascalmpl.value.type.Type;
import org.rascalmpl.value.type.TypeStore;
import org.rascalmpl.value.visitors.IValueVisitor;
/**
* This class implements the standard readable syntax for {@link IValue}'s.
* See also {@link StandardTextReader}
*/
public class StandardTextWriter implements IValueTextWriter {
protected final boolean indent;
protected final int tabSize;
public StandardTextWriter() {
this(false);
}
public StandardTextWriter(boolean indent) {
this(indent, 2);
}
public StandardTextWriter(boolean indent, int tabSize) {
this.indent = indent;
this.tabSize = tabSize;
}
public static String valueToString(IValue value) {
try(StringWriter stream = new StringWriter()) {
new StandardTextWriter().write(value, stream);
return stream.toString();
} catch (IOException ioex) {
throw new RuntimeException("Should have never happened.", ioex);
}
}
public void write(IValue value, java.io.Writer stream) throws IOException {
try {
value.accept(new Writer(stream, indent, tabSize));
}
finally {
stream.flush();
}
}
public void write(IValue value, java.io.Writer stream, TypeStore typeStore) throws IOException {
write(value, stream);
}
protected static class Writer implements IValueVisitor<IValue, IOException> {
private final java.io.Writer stream;
private final int tabSize;
private final boolean indent;
private int tab = 0;
public Writer(java.io.Writer stream, boolean indent, int tabSize) {
this.stream = stream;
this.indent = indent;
this.tabSize = tabSize;
}
private void append(String string) throws IOException {
stream.write(string);
}
private void append(char c) throws IOException {
stream.write(c);
}
private void tab() {
this.tab++;
}
private void untab() {
this.tab--;
}
public IValue visitBoolean(IBool boolValue)
throws IOException {
append(boolValue.getValue() ? "true" : "false");
return boolValue;
}
public IValue visitReal(IReal o) throws IOException {
append(o.getStringRepresentation());
return o;
}
public IValue visitInteger(IInteger o) throws IOException {
append(o.getStringRepresentation());
return o;
}
public IValue visitRational(IRational o) throws IOException {
append(o.getStringRepresentation());
return o;
}
public IValue visitList(IList o) throws IOException {
append('[');
boolean indent = checkIndent(o);
Iterator<IValue> listIterator = o.iterator();
tab();
indent(indent);
if(listIterator.hasNext()){
listIterator.next().accept(this);
while(listIterator.hasNext()){
append(',');
if (indent) indent();
listIterator.next().accept(this);
}
}
untab();
indent(indent);
append(']');
return o;
}
public IValue visitMap(IMap o) throws IOException {
append('(');
tab();
boolean indent = checkIndent(o);
indent(indent);
Iterator<IValue> mapIterator = o.iterator();
if(mapIterator.hasNext()){
IValue key = mapIterator.next();
key.accept(this);
append(':');
o.get(key).accept(this);
while(mapIterator.hasNext()){
append(',');
indent(indent);
key = mapIterator.next();
key.accept(this);
append(':');
o.get(key).accept(this);
}
}
untab();
indent(indent);
append(')');
return o;
}
public IValue visitConstructor(IConstructor o) throws IOException {
String name = o.getName();
if (name == null)
System.err.println("hello");
if (name.equals("loc")) {
append('\\');
}
if (name.indexOf('-') != -1) {
append('\\');
}
append(name);
boolean indent = checkIndent(o);
append('(');
tab();
indent(indent);
Iterator<IValue> it = o.iterator();
int k = 0;
while (it.hasNext()) {
it.next().accept(this);
if (it.hasNext()) {
append(',');
indent(indent);
}
k++;
}
if (o.mayHaveKeywordParameters()) {
IWithKeywordParameters<? extends IConstructor> wkw = o.asWithKeywordParameters();
if (wkw.hasParameters()) {
if (k > 0) {
append(',');
}
Iterator<Entry<String, IValue>> iterator = wkw.getParameters().entrySet().iterator();
while (iterator.hasNext()) {
Entry<String,IValue> e = iterator.next();
append(e.getKey());
append('=');
e.getValue().accept(this);
if (iterator.hasNext()) {
append(',');
}
}
}
}
append(')');
untab();
if (o.isAnnotatable() && o.asAnnotatable().hasAnnotations()) {
append('[');
tab();
indent();
int i = 0;
Map<String, IValue> annotations = o.asAnnotatable().getAnnotations();
for (Entry<String, IValue> entry : annotations.entrySet()) {
append("@" + entry.getKey() + "=");
entry.getValue().accept(this);
if (++i < annotations.size()) {
append(",");
indent();
}
}
untab();
indent();
append(']');
}
try {
stream.flush();
}
catch (IOException e) {
// flushing is just to make sure we get some intermediate output
}
return o;
}
private void indent() throws IOException {
indent(indent);
}
private void indent(boolean indent) throws IOException {
if (indent) {
append('\n');
for (int i = 0; i < tabSize * tab; i++) {
append(' ');
}
}
}
public IValue visitRelation(ISet o) throws IOException {
return visitSet(o);
}
public IValue visitSet(ISet o) throws IOException {
append('{');
boolean indent = checkIndent(o);
tab();
indent(indent);
Iterator<IValue> setIterator = o.iterator();
if(setIterator.hasNext()){
setIterator.next().accept(this);
while(setIterator.hasNext()){
append(",");
indent(indent);
setIterator.next().accept(this);
}
}
untab();
indent(indent);
append('}');
return o;
}
private boolean checkIndent(ISet o) {
if (indent && o.size() > 1) {
for (IValue x : o) {
Type type = x.getType();
return indented(type);
}
}
return false;
}
private boolean indented(Type type) {
return type.accept(new ITypeVisitor<Boolean,RuntimeException>() {
@Override
public Boolean visitReal(Type type) {
return false;
}
@Override
public Boolean visitInteger(Type type) {
return false;
}
@Override
public Boolean visitRational(Type type) {
return false;
}
@Override
public Boolean visitList(Type type) {
return true;
}
@Override
public Boolean visitMap(Type type) {
return true;
}
@Override
public Boolean visitNumber(Type type) {
return false;
}
@Override
public Boolean visitAlias(Type type) {
return type.getAliased().accept(this);
}
@Override
public Boolean visitSet(Type type) {
return true;
}
@Override
public Boolean visitSourceLocation(Type type) {
return true;
}
@Override
public Boolean visitString(Type type) {
return false;
}
@Override
public Boolean visitNode(Type type) {
return true;
}
@Override
public Boolean visitConstructor(Type type) {
return true;
}
@Override
public Boolean visitAbstractData(Type type) {
return true;
}
@Override
public Boolean visitTuple(Type type) {
return true;
}
@Override
public Boolean visitValue(Type type) {
return false;
}
@Override
public Boolean visitVoid(Type type) {
return false;
}
@Override
public Boolean visitBool(Type type) {
return false;
}
@Override
public Boolean visitParameter(Type type) {
return type.getBound().accept(this);
}
@Override
public Boolean visitExternal(Type type) {
return false;
}
@Override
public Boolean visitDateTime(Type type) {
return false;
}
});
}
private boolean checkIndent(IList o) {
if (indent && o.length() > 1) {
for (IValue x : o) {
Type type = x.getType();
if (indented(type)) {
return true;
}
}
}
return false;
}
private boolean checkIndent(INode o) {
if (indent && o.arity() > 1) {
for (IValue x : o) {
Type type = x.getType();
if (indented(type)) {
return true;
}
}
}
return false;
}
private boolean checkIndent(IMap o) {
if (indent && o.size() > 1) {
for (IValue x : o) {
Type type = x.getType();
Type vType = o.get(x).getType();
if (indented(type)) {
return true;
}
if (indented(vType)) {
return true;
}
}
}
return false;
}
public IValue visitSourceLocation(ISourceLocation o)
throws IOException {
append('|');
append(o.getURI().toString());
append('|');
if (o.hasOffsetLength()) {
append('(');
append(Integer.toString(o.getOffset()));
append(',');
append(Integer.toString(o.getLength()));
if (o.hasLineColumn()) {
append(',');
append('<');
append(Integer.toString(o.getBeginLine()));
append(',');
append(Integer.toString(o.getBeginColumn()));
append('>');
append(',');
append('<');
append(Integer.toString(o.getEndLine()));
append(',');
append(Integer.toString(o.getEndColumn()));
append('>');
}
append(')');
}
return o;
}
public IValue visitString(IString o) throws IOException {
printString(o.getValue());
return o;
}
private void printString(String o) throws IOException {
append('\"');
char[] chars = o.toCharArray();
for (int i = 0; i < chars.length; i++) {
char ch = chars[i];
switch (ch) {
case '\"':
append('\\');
append('\"');
break;
case '>':
append('\\');
append('>');
break;
case '<':
append('\\');
append('<');
break;
case '\'':
append('\\');
append('\'');
break;
case '\\':
append('\\');
append('\\');
break;
case '\n':
append('\\');
append('n');
break;
case '\r':
append('\\');
append('r');
break;
case '\t':
append('\\');
append('t');
break;
case ' ':
// needed because other space chars will be escaped in the default branch
append(' ');
break;
default:
int cp = Character.codePointAt(chars, i);
if (Character.isSpaceChar(cp)
|| Character.isISOControl(cp)
|| Character.UnicodeBlock.SPECIALS.equals(Character.UnicodeBlock.of(cp))) {
// these characters are invisible or otherwise unreadable and we escape them here
// for clarity of the serialized string
if (cp <= Byte.MAX_VALUE) {
append("\\a" + String.format("%02x", (int) ch));
}
else if (cp <= Character.MAX_VALUE) {
append("\\u" + String.format("%04x", (int) ch));
}
else {
append("\\U" + String.format("%06x", (int) ch));
}
if (Character.isHighSurrogate(ch)) {
i++; // skip the next char
}
}
else {
append(ch);
if (Character.isHighSurrogate(ch) && i + 1 < chars.length) {
append(chars[++i]);
}
}
}
}
append('\"');
}
public IValue visitTuple(ITuple o) throws IOException {
append('<');
Iterator<IValue> it = o.iterator();
if (it.hasNext()) {
it.next().accept(this);
}
while (it.hasNext()) {
append(',');
it.next().accept(this);
}
append('>');
return o;
}
public IValue visitExternal(IExternalValue externalValue) throws IOException {
return visitConstructor(externalValue.encodeAsConstructor());
}
public IValue visitDateTime(IDateTime o) throws IOException {
append("$");
if (o.isDate()) {
append(String.format("%04d", o.getYear()));
append("-");
append(String.format("%02d", o.getMonthOfYear()));
append("-");
append(String.format("%02d", o.getDayOfMonth()));
} else if (o.isTime()) {
append("T");
append(String.format("%02d", o.getHourOfDay()));
append(":");
append(String.format("%02d", o.getMinuteOfHour()));
append(":");
append(String.format("%02d", o.getSecondOfMinute()));
append(".");
append(String.format("%03d", o.getMillisecondsOfSecond()));
if (o.getTimezoneOffsetHours() < 0 || (o.getTimezoneOffsetHours() == 0 && o.getTimezoneOffsetMinutes() < 0))
append("-");
else
append("+");
append(String.format("%02d", Math.abs(o.getTimezoneOffsetHours())));
append(":");
append(String.format("%02d", Math.abs(o.getTimezoneOffsetMinutes())));
} else {
append(String.format("%04d", o.getYear()));
append("-");
append(String.format("%02d", o.getMonthOfYear()));
append("-");
append(String.format("%02d", o.getDayOfMonth()));
append("T");
append(String.format("%02d", o.getHourOfDay()));
append(":");
append(String.format("%02d", o.getMinuteOfHour()));
append(":");
append(String.format("%02d", o.getSecondOfMinute()));
append(".");
append(String.format("%03d", o.getMillisecondsOfSecond()));
if (o.getTimezoneOffsetHours() < 0 || (o.getTimezoneOffsetHours() == 0 && o.getTimezoneOffsetMinutes() < 0))
append("-");
else
append("+");
append(String.format("%02d", Math.abs(o.getTimezoneOffsetHours())));
append(":");
append(String.format("%02d", Math.abs(o.getTimezoneOffsetMinutes())));
}
append("$");
return o;
}
public IValue visitListRelation(IList o)
throws IOException {
visitList(o);
return o;
}
public IValue visitNode(INode o) throws IOException {
printString(o.getName());
boolean indent = checkIndent(o);
append('(');
tab();
indent(indent);
Iterator<IValue> it = o.iterator();
int k = 0;
while (it.hasNext()) {
it.next().accept(this);
if (it.hasNext()) {
append(',');
indent(indent);
}
k++;
}
if (o.mayHaveKeywordParameters()) {
IWithKeywordParameters<? extends INode> wkw = o.asWithKeywordParameters();
if (wkw.hasParameters()) {
if (k > 0) {
append(',');
}
Iterator<Entry<String,IValue>> kwIt = wkw.getParameters().entrySet().iterator();
while (kwIt.hasNext()) {
Entry<String, IValue> e = kwIt.next();
indent();
append(e.getKey());
append('=');
e.getValue().accept(this);
if (kwIt.hasNext()) {
append(',');
}
}
}
}
append(')');
untab();
if (o.isAnnotatable() && o.asAnnotatable().hasAnnotations()) {
append('[');
tab();
indent();
int i = 0;
Map<String, IValue> annotations = o.asAnnotatable().getAnnotations();
for (Entry<String, IValue> entry : annotations.entrySet()) {
append("@" + entry.getKey() + "=");
entry.getValue().accept(this);
if (++i < annotations.size()) {
append(",");
indent();
}
}
untab();
indent();
append(']');
}
return o;
}
}
}