/* * 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.Map; import java.util.Iterator; import java.io.EOFException; import java.util.ConcurrentModificationException; import org.apache.sis.util.Numbers; import org.apache.sis.util.CharSequences; import org.apache.sis.metadata.MetadataStandard; /** * Helper class for parsing the lines of a JSON file in the format described by {@link Template}. * The parsed file may be either the template, or a file obtained after insertion of values in the template. * * @author Martin Desruisseaux (Geomatys) */ final class LineReader { /** * The {@code null} string. */ private static final String NULL = "null"; /** * The metadata standard for the {@link TemplateNode} to create. */ final MetadataStandard standard; /** * Pool of strings, for replacing duplicated instances (which are numerous) by shared instances. * May be {@code null} if we do not need to share the line instances. */ private final Map<String,String> sharedLines; /** * For sharing same {@code String[]} instances when possible. This sharing is helpful for a * {@link NumerotedPath} optimization, since it will allow to compare paths by reference * before to perform full {@code Arrays.equals(…)} checks. */ private final Map<String,String[]> sharedPaths; /** * An iterator over the lines to read. */ private final Iterator<? extends CharSequence> lines; /** * The line currently being parsed. */ private String line; /** * The line length, excluding the trailing comma (if any) and trailing whitespaces. */ private int length; /** * The current position in {@link #line}. */ private int position; /** * Non-null if the current {@linkplain #line} has a trailing comma, ignoring whitespaces. * If non-null, the value is either "," or ",{". * * @see #skipComma() */ private String trailingComma; /** * Creates a new parser. * * @param standard The standard used by the metadata objects to write. * @param template The JSON lines to use as a template. * @param sharedLines An initially empty map to be filled by {@code LineReader} * for sharing same {@code String} instances when possible. */ LineReader(final MetadataStandard standard, final Iterable<? extends CharSequence> lines, final Map<String,String> sharedLines, final Map<String,String[]> sharedPaths) { this.standard = standard; this.sharedLines = sharedLines; this.sharedPaths = sharedPaths; this.lines = lines.iterator(); } /** * Returns the next line. */ String nextLine() throws EOFException { trailingComma = null; if (!lines.hasNext()) { throw new EOFException("Unexpected end of file."); } line = sharedLine(lines.next().toString()); length = CharSequences.skipTrailingWhitespaces(line, 0, line.length()); position = CharSequences.skipLeadingWhitespaces (line, 0, length); final int remaining = length - position; if (remaining >= 1) { switch (line.charAt(length - 1)) { case ',': { trailingComma = ","; break; } case '{': { if (remaining >= 2 && line.charAt(length - 2) == ',') { trailingComma = ",{"; } break; } } if (trailingComma != null) { length = CharSequences.skipTrailingWhitespaces(line, position, length - trailingComma.length()); } } return line; } /** * Returns the line length, excluding the trailing comma (if any) and trailing whitespaces. */ short length() throws ParseException { if ((length & ~Short.MAX_VALUE) == 0) { return (short) length; } throw new ParseException("Line too long."); } /** * Returns {@code true} if the current line is empty or contains only whitespaces, ignoring the trailing comma. */ boolean isEmpty() { return position >= length; } /** * Returns {@code true} if the current line starting at the current position contains the given substring. * If there is a match, then {@link #position} is set to the first character after the substring. */ boolean regionMatches(final String s) { if (line.regionMatches(position, s, 0, s.length())) { position += s.length(); return true; } return false; } /** * Skips the {@code ':'} separator in the current line, then returns the value. * The position is set after the value, which is usually {@link #length}. * * @return The value (may be {@code null}, a {@link String} or a {@link Number}). * @throws ParseException If the line doesn't have the expected syntax. */ Object getValue() throws ParseException { Exception cause = null; position = CharSequences.skipLeadingWhitespaces(line, position, length); if (position < length && line.charAt(position) == ':') { position = CharSequences.skipLeadingWhitespaces(line, position+1, length); if (position < length) { if (regionMatches(NULL)) { if (position == length) { return null; } } else if (line.charAt(position) == '"' && line.charAt(length-1) == '"') { final int p = position + 1; position = length; CharSequence cs = line.substring(p, length - 1); cs = CharSequences.replace(cs, "\\\"", "\""); cs = CharSequences.replace(cs, "\\n", "\n"); cs = CharSequences.replace(cs, "\\t", "\t"); return cs.toString(); } else try { final Number value = Numbers.narrowestNumber(line.substring(position, length)); position = length; return value; } catch (NumberFormatException e) { cause = e; } } } throw new ParseException("Invalid \"name\":\"value\" pair:\n" + line, cause); } /** * Returns {@code true} if the current line has a trailing comma, ignoring whitespaces. * The comma may be "," alone, or the comma followed by an opening bracket ",{". */ boolean hasTrailingComma() { return trailingComma != null; } /** * Skips the comma, if presents. Leading whitespaces are ignored. * * @return Non-null if a comma was present. */ String skipComma() { position = CharSequences.skipLeadingWhitespaces(line, position, length); if (position >= length) { return trailingComma; } if (line.charAt(position) == ',') { position++; return ","; } return null; } /** * Increment or decrement the given level depending on the amount of { or } characters * in the current portion of the current line. The {@linkplain #position} is advanced * until after the } character of level 0, or until the end of line. */ int updateLevel(int level) { final int upper = line.length(); boolean quote = false; scan: while (position < upper) { switch (line.charAt(position++)) { case '"': { if (position <= 1 || line.charAt(position - 2) != '\\') { quote = !quote; } break; } case '{': { if (!quote) { level++; } break; } case '}': { if (!quote && --level == 0) { break scan; } break; } } } return level; } /** * Returns the currently selected part of current line. */ @Override public String toString() { return (line != null) ? line.substring(position, length) : ""; } /** * Returns the current line (in full, not only the current portion) except for the value. */ String fullLineWithoutValue() { return sharedLine(line.substring(0, line.indexOf(':') + 1)); } /** * Returns the current line (in full, not only the current portion) without the trailing "null". */ String fullLineWithoutNull() throws ParseException { if (trailingComma != null) { throw new ParseException("Value shall be the last entry in a field."); } if (getValue() != null) { throw new ParseException("Value of \"value\" shall be null."); } return sharedLine(line.substring(0, length - NULL.length())); } /** * Adds the given line to the pool if absent, or returns the existing line otherwise. * * @todo Use Map.putIfAbsent when we will be allowed to compile for JDK8. */ private String sharedLine(final String newLine) { if (sharedLines != null) { final String existing = sharedLines.get(newLine); if (existing != null) { return existing; } if (sharedLines.put(newLine, newLine) != null) { throw new ConcurrentModificationException(); } } return newLine; } /** * Split the given path and {@linkplain String#intern() internalize} the components. * We internalize the components because they usually already exists elsewhere in the JVM, * as field names or annotation values. */ String[] sharedPath(final String path) { String[] ci = sharedPaths.get(path); if (ci == null) { final CharSequence[] c = CharSequences.split(path, Keywords.PATH_SEPARATOR); ci = new String[c.length]; for (int i=0; i<c.length; i++) { ci[i] = c[i].toString().intern(); } if (sharedPaths.put(path, ci) != null) { throw new ConcurrentModificationException(); } } return ci; } }