/* * 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.SortedMap; import java.util.TreeMap; import java.util.List; import java.util.Arrays; import java.util.ArrayList; import java.util.ConcurrentModificationException; import java.io.IOException; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import org.opengis.util.FactoryException; import org.apache.sis.metadata.AbstractMetadata; import org.apache.sis.metadata.MetadataStandard; import org.apache.sis.util.CharSequences; import org.apache.sis.util.logging.Logging; import org.constellation.json.metadata.binding.*; /** * Reads a JSON file containing the values provided by end user from the web interfaces, * and store those values in a metadata object. * * @author Martin Desruisseaux (Geomatys) */ final class FormReader { /** * @deprecated "Log and continue" is not appropriate since the user can not know that his data is lost * (especially when the client is a web browser and the logging occurs on the server). This field needs * to be deleted and the logging replaced by an exception or any other mechanism reporting error to the * user. */ private final static Logger LOGGER = Logging.getLogger("org.constellation.json.metadata"); /** * For iterating over the lines of the JSON file to parse. */ private final LineReader parser; /** * {@code true} for invoking {@link LineReader#nextLine()} for the first line, or * {@code false} for continuing the parsing from the current {@link LineReader} content. */ private boolean isNextLineRequested; /** * Indices of path elements. The 0 value means that the corresponding path element does * not specify any index. */ private final int[] indices; /** * {@code true} for skipping {@code null} values instead than storing null in the metadata object. * See the {@code skipNulls} argument of {@link Template#read(Iterable, Object, boolean)} * for more information. */ private final boolean skipNulls; /** * The values for each path found in the file. Values can only be instances of {@link String}, * {@link Number} or {@code List<Object>}. * * If the list of legal types is modified, consider revisiting {@link MetadataUpdater#value}. */ private final SortedMap<NumerotedPath,Object> values; /** * The GeoAPI interfaces substitution. * For example {@code Identification} is typically interpreted as {@code DataIdentification}. */ private final Map<Class<?>, Class<?>> specialized; /** * Creates a new form reader. */ FormReader(final LineReader parser, final int maxDepth, final boolean skipNulls, final Map<Class<?>, Class<?>> specialized) { this.parser = parser; this.indices = new int[maxDepth]; this.skipNulls = skipNulls; this.specialized = specialized; isNextLineRequested = true; values = new TreeMap<>(); } /** * Parses the given JSON lines. * * <p>This method invokes itself recursively.</p> * * @param parent The parent path, or {@code null} if none. * @throws IOException if an error occurred while parsing. */ final void read(final String[] parent) throws IOException { String[] path = null; int level = 0; while (true) { if (isNextLineRequested) { parser.nextLine(); } if (parser.regionMatches(Keywords.PATH)) { path = parsePath(parent, (String) parser.getValue()); } else if (parser.regionMatches(Keywords.VALUE)) { addValue(path, parser.getValue()); } else if (parser.regionMatches(Keywords.CHILDREN)) { do { isNextLineRequested = false; read(path); } while (parser.skipComma() != 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(); } } /** * Parses the given JSON object. * * @throws ParseException if an error occurred while parsing. */ final void read(final RootObj root) throws ParseException { for (final SuperBlockObj sb : root.getRoot().getChildren()) { for (final BlockObj bc : sb.getSuperblock().getChildren()) { final Block block = bc.getBlock(); final String[] parent = parsePath(null, block.getPath()); for (final ComponentObj fc : block.getChildren()) { final Field field = ((FieldObj)fc).getField(); final String[] path = parsePath(parent, field.getPath()); addValue(path, field.value); } } } } /** * Parses the given path and returns its component. * This method returns the path components and stores the index values in the {@link #indices} array. * * @param parent The component of the parent path, or {@code null} if none. * @param path The path to parse. * @return The path components. */ private String[] parsePath(final String[] parent, final String path) throws ParseException { if (path == null) { return null; } final String[] components = (String[]) CharSequences.split(path, Keywords.PATH_SEPARATOR); if (components.length > indices.length) { // TODO: "log and continue" is not appropriate here, since the user can not know that his data is lost. LOGGER.log(Level.WARNING, "Error parsing path:{0}", path); } Arrays.fill(indices, 0, components.length, 0); NumberFormatException cause = null; for (int i=0; i<components.length; i++) { String p = components[i]; final int lower = p.lastIndexOf('['); if (lower >= 0) { final int upper = p.lastIndexOf(']'); boolean failed = (upper <= lower); if (!failed) try { indices[i] = Integer.parseInt(p.substring(lower + 1, upper)); } catch (NumberFormatException e) { cause = e; failed = true; } if (failed) { throw new ParseException(formatPath("Illegal path syntax: \"", components, "\"."), cause); } components[i] = p.substring(0, lower); // Set only in case of success. } } if (!TemplateNode.startsWith(components, parent)) { throw new ParseException(formatPath("Path \"", components, "\" is inconsistent with parent.")); } return components; } /** * Adds the given metadata value to the {@link #values} map. */ private void addValue(final String[] path, Object value) throws ParseException { if (path == null) { throw new ParseException("Missing path for value: " + value); } if (value instanceof CharSequence && CharSequences.skipTrailingWhitespaces( (CharSequence) value, 0, ((CharSequence) value).length()) <= 0) { value = null; } if (value != null || !skipNulls) { final NumerotedPath key = new NumerotedPath(path, indices); key.ignoreLastIndex(); if (key.isMultiOccurrenceAllowed()) { @SuppressWarnings("unchecked") // 'values' javadoc. List<Object> list = (List<Object>) values.get(key); if (list == null) { list = new ArrayList<>(2); if (values.put(key, list) != null) { throw new ConcurrentModificationException(); } } if (value != null) { // 'null' can mean an empty list, but not null element in the list. list.add(value); } } else if (values.put(key, value) != null) { throw new ParseException(formatPath("Path \"", path, "\" is repeated twice.")); } } } /** * Writes the content of the {@link #values} map in the given metadata object. */ final void writeToMetadata(final MetadataStandard standard, final Object destination) throws ParseException { if (!values.isEmpty()) { final MetadataUpdater updater = new MetadataUpdater(standard, values, specialized); try { updater.update(null, destination); } catch (IllegalArgumentException | ClassCastException | FactoryException e) { System.arraycopy(updater.np.indices, 0, indices, 0, updater.np.indices.length); throw new ParseException(formatPath("Can not store value at path \"", updater.np.path, "\"."), e); } } if (destination instanceof AbstractMetadata) { ((AbstractMetadata) destination).prune(); } } /** * Formats an error message with the given path. */ private String formatPath(final String before, final String[] path, final String after) { final StringBuilder buffer = new StringBuilder(before); try { NumerotedPath.formatPath(buffer, path, 0, indices); } catch (IOException e) { throw new AssertionError(e); // Should never happen, since we are writting to a StringBuilder. } return buffer.append(after).toString(); } }