/*
* Constellation - An open source and standard compliant SDI
* http://www.constellation-sdi.org
*
* Copyright 2014 Geomatys.
*
* 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.constellation.json.metadata;
import java.util.List;
import java.util.ArrayList;
import java.io.IOException;
import org.apache.sis.metadata.MetadataStandard;
import org.apache.sis.util.CharSequences;
/**
* An immutable node in the template. This node contains either {@link String}s, to be copied verbatim,
* or other {@code TemplateNode}s, thus forming a tree.
*
* <p>All instances of {@code TemplateNode} shall be immutable in order to allow concurrent use
* in multi-thread environment.</p>
*
* @author Martin Desruisseaux (Geomatys)
*/
final class TemplateNode {
/**
* The metadata standard for this node.
*/
final MetadataStandard standard;
/**
* The lines or other nodes contained in this node.
* Elements in this array are either {@link String} or other {@code TemplateNode}.
* <strong>Do not modify the content of this array.</strong>
*/
private final Object[] content;
/**
* A subset of {@link #content} containing only the {@code TemplateNode} instances,
* or {@code null} if none. <strong>Do not modify the content of this array.</strong>
*/
final TemplateNode[] children;
/**
* The components of the {@code "path"} value found in the node, or {@code null} if none.
* <strong>Do not modify the content of this array.</strong>
*/
final String[] path;
/**
* If there is paths to ignore, those paths. Otherwise {@code null}.
*/
final NumerotedPath[] ignore;
/**
* The value of the {@code "defaultValue"} element found in the node, or {@code null}.
*/
final Object defaultValue;
/**
* Index of the line where to format the value, or -1 if none.
* The {@code content[valueIndex]} line shall be a {@link String} containing {@code "value:"}.
*/
private final int valueIndex;
/**
* Index of the line where to format the path, or -1 if none.
* The {@code content[pathIndex]} line shall be a {@link String} containing {@code "path:"}.
*/
private final int pathIndex;
/**
* Maximum number of occurrences allowed for this node.
* Must not be negative or zero.
*/
final int maxOccurs;
/**
* {@code true} if the last line has a trailing comma, ignoring whitespaces.
* The comma may be "," alone, or the comma followed by an opening bracket ",{".
*/
private final boolean hasTrailingComma;
/**
* The length of the last line of {@link #content} when we want to omit the trailing comma.
* This is used when the template contains other nodes after this node, but we do not want
* to write those nodes because the remaining nodes are empty and pruned.
*/
private final short lengthWithoutComma;
/**
* The separator between this node and an other occurrence of the same node.
* This field is never {@code null}. Its value is the comma ({@code ','}),
* sometime followed by an opening bracket if the first line of {@link #content}
* does not have an opening bracket.
*/
private final String separator;
/**
* The value of the {@code "render"} property, or {@code null} if none.
*/
final String render;
/**
* Creates a new node for the given lines.
*
* @param parser An iterator over the lines to parse.
* @param isNextLineRequested {@code true} for invoking {@link LineReader#nextLine()} for the first line, or
* {@code false} for continuing the parsing from the current {@link LineReader} content.
* @param separator The separator between this node and an other occurrence of the same node,
* or {@code null} if unknown.
*/
TemplateNode(final LineReader parser, boolean isNextLineRequested, String separator) throws IOException {
final List<Object> content = new ArrayList<>();
final List<TemplateNode> children = new ArrayList<>();
String path = null;
String ignore = null;
String render = null;
Object defaultValue = null;
int valueIndex = -1;
int pathIndex = -1;
int maxOccurs = Integer.MAX_VALUE;
int level = 0;
while (true) {
if (isNextLineRequested) {
content.add(parser.nextLine());
}
if (parser.regionMatches(Keywords.PATH)) {
if (pathIndex >= 0) {
throw new ParseException("Duplicated " + Keywords.PATH + '.');
}
final Object p = parser.getValue();
if (p != null) {
path = p.toString();
pathIndex = content.size() - 1;
content.set(pathIndex, parser.fullLineWithoutValue());
}
} else if (parser.regionMatches(Keywords.IGNORE)) {
ignore = (String) parser.getValue();
} else if (parser.regionMatches(Keywords.RENDER)) {
render = (String) parser.getValue();
} else if (parser.regionMatches(Keywords.DEFAULT_VALUE)) {
defaultValue = parser.getValue();
} else if (parser.regionMatches(Keywords.VALUE)) {
if (valueIndex >= 0) {
throw new ParseException("Duplicated " + Keywords.VALUE + '.');
}
valueIndex = content.size() - 1;
content.set(valueIndex, parser.fullLineWithoutNull());
} else if (parser.regionMatches(Keywords.MAX_OCCURRENCES)) {
final Object n = parser.getValue();
if (!(n instanceof Number) || (maxOccurs = ((Number) n).intValue()) < 1) {
throw new ParseException("Invalid multiplicity: " + n);
}
} else if (parser.regionMatches(Keywords.CHILDREN)) {
String childSeparator = null;
do {
final TemplateNode child = new TemplateNode(parser, false, childSeparator);
content .add(child);
children.add(child);
childSeparator = parser.skipComma();
} while (childSeparator != null);
}
/*
* Increment or decrement the level when we find '{' or '}' character. The loop exits when we reach
* back the level 0, except if the line was not part of this node (isNextLineRequested == false),
* because the opening bracket may be on the next line. Remaining lines are ignored.
*/
level = parser.updateLevel(level);
if (level == 0 && isNextLineRequested) {
break;
}
isNextLineRequested = parser.isEmpty();
}
/*
* If the separator was not declared (which happen only for the first element of a new array),
* infers it from whether or not the first non-white line in this node has an opening bracket.
*/
if (separator == null) {
separator = ",";
for (final Object e : content) {
if (e instanceof TemplateNode) {
separator = ",{";
break;
}
final String line = (String) e;
final int i = CharSequences.skipLeadingWhitespaces(line, 0, line.length());
if (i < line.length()) {
if (line.charAt(i) != '{') {
separator = ",{";
}
break;
}
}
}
/*
* Finally store all the information we just computed.
*/
this.standard = parser.standard;
this.content = content.toArray();
this.children = children.isEmpty() ? null : children.toArray(new TemplateNode[children.size()]);
this.path = (path != null) ? parser.sharedPath(path) : null;
this.ignore = (ignore != null) ? NumerotedPath.parse(this.path, ignore) : null;
this.defaultValue = defaultValue;
this.valueIndex = valueIndex;
this.pathIndex = pathIndex;
this.maxOccurs = maxOccurs;
this.hasTrailingComma = parser.hasTrailingComma();
this.lengthWithoutComma = parser.length();
this.separator = separator;
this.render = render;
}
/**
* Returns {@code true} if the given path starts with the given prefix.
* A null {@code prefix} is considered synonymous to an empty prefix.
*/
static boolean startsWith(final CharSequence[] path, final CharSequence[] prefix) {
if (prefix != null) {
if (prefix.length > path.length) {
return false;
}
for (int i=0; i<prefix.length; i++) {
if (!path[i].equals(prefix[i])) {
return false;
}
}
}
return true;
}
/**
* Returns {@code true} if this path ends with the given suffix.
*/
final boolean endsWith(final CharSequence[] suffix) {
int j = path.length - suffix.length;
if (j < 0) {
return false;
}
for (int i=0; i<suffix.length; i++, j++) {
if (!path[j].equals(suffix[i])) {
return false;
}
}
return true;
}
/**
* Validates the {@link #path} of this node and all child nodes.
* This method shall be invoked on the root node after we finished to build the whole tree.
* This method invokes itself recursively for validating children too.
*
* @return The maximal length of {@code path} arrays found in the tree.
*/
final int validatePath(CharSequence[] prefix) throws ParseException {
int depth = 0;
if (path != null) {
if (!startsWith(path, prefix)) {
final StringBuilder buffer = new StringBuilder("Path ");
appendPath(0, buffer);
throw new ParseException(buffer.append(" is inconsistent with parent.").toString());
}
prefix = path;
depth = prefix.length;
}
if (children != null) {
for (final TemplateNode template : children) {
final int c = template.validatePath(prefix);
if (c > depth) {
depth = c;
}
}
}
return depth;
}
/**
* Appends the path (including quotes) for this node in the given buffer.
* Callers must ensure that {@link #path} is non-null before to invoke this method.
*/
final void appendPath(final int pathOffset, final StringBuilder appendTo) {
appendTo.append('"');
if (pathOffset != 0) {
appendTo.append('(');
}
for (int i=0; i<path.length; i++) {
if (i != 0) {
if (i == pathOffset) {
appendTo.append(')');
}
appendTo.append(Keywords.PATH_SEPARATOR);
}
appendTo.append(path[i]);
}
appendTo.append('"');
}
/**
* Returns {@code true} if this node contains a "value" property.
*/
final boolean isField() {
return valueIndex >= 0;
}
/**
* Writes the given metadata to the given output using this node as a template.
*
* @param metadata The metadata to write.
* @param out Where to write the JSON file.
* @param prune {@code true} for omitting empty nodes.
* @param maxDepth The maximal length of {@link #path} in this node and child nodes.
* @throws IOException If an error occurred while writing the JSON file.
*/
final void write(final Object metadata, final Appendable out, final boolean prune, final int maxDepth) throws IOException {
final TemplateApplicator f = new TemplateApplicator(prune, maxDepth);
final ValueNode[] tree = f.createValueTree(this, metadata);
if (tree != null) {
for (final ValueNode root : tree) {
writeTree(root, out, true);
}
}
}
/**
* Writes the given metadata to the given output using this node as a template.
*
* @param metadata The metadata to write.
* @param node The node and its list of children computed by {@link TemplateApplicator#createValueTree}.
* @param out Where to write the JSON file.
* @throws IOException If an error occurred while writing the JSON file.
*/
private void writeTree(final ValueNode node, final Appendable out, final boolean isLastNode) throws IOException {
boolean hasEmptyNode = false;
for (int i=0; i<content.length; i++) {
final Object line = content[i];
if (line instanceof TemplateNode) {
/*
* If the "line" is actually a "superblock", "block" or "field", we may have many occurrences of
* that node. Formats an occurrence for each value. Note that if we have more than one occurrence
* but the node was used to be the last array element, we will need to append a separator. This is
* usually "," but can also be ",{" if the node has no opening bracket.
*/
final int n = node.size();
if (n == 0 && !hasEmptyNode) { // HACK! The 'node' list should never be empty at this point.
hasEmptyNode = true; // I don't know where is the bug. But at least, let try to have
out.append('}'); // valid JSON.
}
for (int j=0; j<n; j++) {
final ValueNode child = node.get(j);
if (child.template == line) {
child.template.writeTree(child, out, (j+1) == n);
}
}
} else {
/*
* If the line is an ordinary line, write that line with the following special cases:
*
* - Append the value if the line is the one which is expected to contain the value.
* - Append "," or ",{" if there is no separator while we are expecting to write more nodes.
* - Or (opposite of above), remove separator if present while we formatted the last node.
*/
final boolean isLastLine = (i+1) == content.length;
if (isLastLine && isLastNode) {
out.append((String) line, 0, lengthWithoutComma);
} else {
out.append((String) line);
}
if (i == pathIndex) { // Implies non-null path.
node.formatPath(out, 0);
out.append(',');
}
if (i == valueIndex) {
node.formatValue(out);
}
if (isLastLine && !isLastNode && !hasTrailingComma) {
out.append(separator);
}
out.append('\n');
}
}
}
/**
* Return the content, for debugging purpose only.
*/
@Override
public String toString() {
if (content == null) {
return "<init>"; // When invoked from the constructor by IDE debugger.
}
final StringBuilder buffer = new StringBuilder(4000);
toString(buffer, 0, 0);
return buffer.append('\n').toString();
}
/**
* Implementation of {@link #toString()} to be invoked recursively by children.
* This method does not add the final EOL character - it is caller responsibility to append it.
*/
private void toString(final StringBuilder buffer, final int indentation, int pathOffset) {
buffer.append(CharSequences.spaces(indentation)).append(isField() ? "Field" : "Node").append('[');
if (path != null) {
appendPath(pathOffset, buffer.append("path:"));
pathOffset += path.length; // For the iteration over children.
}
if (defaultValue != null) {
if (path != null) buffer.append(", ");
buffer.append("defaultValue:\"").append(defaultValue).append('"');
}
buffer.append(']');
if (children != null) {
for (final TemplateNode template : children) {
template.toString(buffer.append('\n'), indentation + 4, pathOffset);
}
}
if (hasTrailingComma) {
if (children != null) {
buffer.append('\n').append(CharSequences.spaces(indentation));
}
buffer.append(',');
}
}
}