You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2015/05/19 01:31:27 UTC

svn commit: r1680131 - in /sis/branches/JDK8/core: sis-metadata/src/main/java/org/apache/sis/io/wkt/ sis-metadata/src/test/java/org/apache/sis/io/wkt/ sis-metadata/src/test/java/org/apache/sis/test/suite/ sis-utility/src/main/java/org/apache/sis/util/r...

Author: desruisseaux
Date: Mon May 18 23:31:27 2015
New Revision: 1680131

URL: http://svn.apache.org/r1680131
Log:
Referencing: starting the port of WKT parser.

Added:
    sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/Element.java   (with props)
    sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/Parser.java   (with props)
    sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/io/wkt/ElementTest.java   (with props)
Modified:
    sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/Symbols.java
    sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/test/suite/MetadataTestSuite.java
    sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
    sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
    sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties

Added: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/Element.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/Element.java?rev=1680131&view=auto
==============================================================================
--- sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/Element.java (added)
+++ sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/Element.java [UTF-8] Mon May 18 23:31:27 2015
@@ -0,0 +1,574 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.sis.io.wkt;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.io.PrintWriter;
+import java.text.ParsePosition;
+import java.text.ParseException;
+import org.apache.sis.util.Debug;
+import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.Exceptions;
+import org.apache.sis.util.CharSequences;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.internal.metadata.WKTKeywords;
+
+import static org.apache.sis.util.collection.Containers.isNullOrEmpty;
+import static org.apache.sis.util.CharSequences.skipLeadingWhitespaces;
+
+
+/**
+ * An element in a <cite>Well Know Text</cite> (WKT). An {@code Element} is made of {@link String},
+ * {@link Number} and other {@link Element}. For example:
+ *
+ * {@preformat text
+ *     PrimeMeridian[“Greenwich”, 0.0, AngleUnit[“degree”, 0.017453292519943295]]]
+ * }
+ *
+ * Each {@code Element} object can contain an arbitrary amount of other elements.
+ * The result is a tree, which can be printed with {@link #print(PrintWriter, int)} for debugging purpose.
+ * Elements can be pulled in a <cite>first in, first out</cite> order.
+ *
+ * @author  Rémi Ève (IRD)
+ * @author  Martin Desruisseaux (IRD, Geomatys)
+ * @since   0.6
+ * @version 0.6
+ * @module
+ */
+final class Element {
+    /**
+     * Hard-coded list of elements in which to parse values as dates instead than numbers.
+     * We may try to find a more generic approach in a future version.
+     */
+    private static final String[] TEMPORAL = {
+        WKTKeywords.TimeOrigin,
+        WKTKeywords.TimeExtent
+    };
+
+    /**
+     * The position where this element starts in the string to be parsed.
+     */
+    private final int offset;
+
+    /**
+     * Keyword of this entity. For example: {@code "PrimeMeridian"}.
+     */
+    public final String keyword;
+
+    /**
+     * An ordered list of {@link String}s, {@link Number}s and other {@link Element}s.
+     * May be {@code null} if the keyword was not followed by a pair of brackets (e.g. "north").
+     */
+    private final List<Object> list;
+
+    /**
+     * Constructs a root element.
+     *
+     * @param singleton The only children for this root.
+     */
+    Element(final Element singleton) {
+        offset  = 0;
+        keyword = null;
+        list    = new LinkedList<>();
+        list.add(singleton);
+    }
+
+    /**
+     * Constructs a new {@code Element}.
+     *
+     * @param text     The text to parse.
+     * @param position On input, the position where to start parsing from.
+     *                 On output, the first character after the separator.
+     */
+    Element(final Parser parser, final String text, final ParsePosition position) throws ParseException {
+        /*
+         * Find the first keyword in the specified string. If a keyword is found, then
+         * the position is set to the index of the first character after the keyword.
+         */
+        offset = position.getIndex();
+        final int length = text.length();
+        int lower = skipLeadingWhitespaces(text, offset, length);
+        { // This block is for keeping some variables local.
+            int c = text.codePointAt(lower);
+            if (!Character.isUnicodeIdentifierStart(c)) {
+                keyword = text;
+                position.setErrorIndex(lower);
+                throw unparsableString(parser, text, position);
+            }
+            int upper = lower;
+            while ((upper += Character.charCount(c)) < length) {
+                c = text.codePointAt(upper);
+                if (!Character.isUnicodeIdentifierPart(c)) break;
+            }
+            keyword = text.substring(lower, upper);
+            lower = skipLeadingWhitespaces(text, upper, length);
+        }
+        final boolean isTemporal = ArraysExt.containsIgnoreCase(TEMPORAL, keyword);
+        /*
+         * At this point we have extracted the keyword (e.g. "PrimeMeridian"). Now parse the opening bracket.
+         * According WKT's specification, two characters are acceptable: '[' and '('. We accept both, but we
+         * will require the matching closing bracket at the end of this method. For example if the opening
+         * bracket was '[', then we will require that the closing bracket is ']' and not ')'.
+         */
+        if (lower >= length) {
+            position.setErrorIndex(lower);
+            list = null;
+            return;
+        }
+        final int openingBracket = text.codePointAt(lower);
+        final int closingBracket = parser.symbols.matchingBracket(openingBracket);
+        if (closingBracket < 0) {
+            position.setErrorIndex(lower);
+            list = null;
+            return;
+        }
+        lower = skipLeadingWhitespaces(text, lower + Character.charCount(openingBracket), length);
+        /*
+         * Parse all elements inside the bracket. Elements are parsed sequentially
+         * and their type are selected according their first character:
+         *
+         *   - If the first character is a quote, then the value is returned as a String.
+         *   - Otherwise, if the first character is a unicode identifier start, then the element is parsed as a chid Element.
+         *   - Otherwise, if the characters are "true" of "false" (ignoring case), then the value is returned as a boolean.
+         *   - Otherwise, the element is parsed as a number or as a date, depending of 'isTemporal' boolean value.
+         */
+        list = new LinkedList<>();
+        final String separator = parser.symbols.trimmedSeparator();
+        do {
+            if (lower >= length) {
+                position.setIndex(offset);
+                position.setErrorIndex(lower);
+                throw missingCharacter(parser, closingBracket, length);
+            }
+            final int c = text.codePointAt(lower);
+            final int closingQuote = parser.symbols.matchingQuote(c);
+            if (closingQuote >= 0) {
+                /*
+                 * Try to parse the next element as a quoted string. We will take it as a string if the first non-blank
+                 * character is a quote.  Note that a double quote means that the quote should be included as-is in the
+                 * parsed text.
+                 */
+                final int n = Character.charCount(closingQuote);
+                lower += Character.charCount(c);
+                CharSequence content = null;
+                do {
+                    final int upper = text.indexOf(closingQuote, lower);
+                    if (upper < lower) {
+                        position.setIndex(offset);
+                        position.setErrorIndex(lower);
+                        throw missingCharacter(parser, closingQuote, lower);
+                    }
+                    if (content == null) {
+                        content = text.substring(lower, upper);   // First text fragment, and usually the only one.
+                    } else {
+                        /*
+                         * We will enter in this block only if we found at least one double quote.
+                         * Convert the first text fragment to a StringBuilder so we can concatenate
+                         * the next text fragments with only one quote between them.
+                         */
+                        if (content instanceof String) {
+                            content = new StringBuilder((String) content);
+                        }
+                        ((StringBuilder) content).appendCodePoint(closingQuote).append(text, lower, upper);
+                    }
+                    lower = upper + n;  // After the closing quote.
+                } while (lower < text.length() && text.codePointAt(lower) == closingQuote);
+                list.add(content.toString());
+            } else if (!Character.isUnicodeIdentifierStart(c)) {
+                /*
+                 * Try to parse the next element as a date or a number. We will attempt such parsing
+                 * if the first non-blank character is not the beginning of an unicode identifier.
+                 * Otherwise we will assume that the next element is the keyword of a child 'Element'.
+                 */
+                position.setIndex(lower);
+                final Object value;
+                if (isTemporal) {
+                    value = parser.parseDate(text, position);
+                } else {
+                    value = parser.parseNumber(text, position);
+                }
+                if (value == null) {
+                    position.setIndex(offset);
+                    // Do not update the error index; it is already updated by NumberFormat.
+                    throw unparsableString(parser, text, position);
+                }
+                list.add(value);
+                lower = position.getIndex();
+            } else if (text.regionMatches(true, lower, "true", 0, 4)) {
+                list.add(Boolean.TRUE);
+                lower += 4;
+            } else if (text.regionMatches(true, lower, "false", 0, 5)) {
+                list.add(Boolean.FALSE);
+                lower += 5;
+            } else {
+                // Otherwise, add the element as a child element.
+                position.setIndex(lower);
+                list.add(new Element(parser, text, position));
+                lower = position.getIndex();
+            }
+            lower = skipLeadingWhitespaces(text, lower, length);
+        } while (text.regionMatches(lower, separator, 0, separator.length()));
+        /*
+         * Verify that the closing bracket is present.
+         */
+        if (lower < length) {
+            final int c = text.codePointAt(lower);
+            if (c == closingBracket) {
+                position.setIndex(lower + Character.charCount(c));
+                return;
+            }
+        }
+        position.setIndex(offset);
+        position.setErrorIndex(lower);
+        throw unparsableString(parser, text, position);
+    }
+
+
+
+
+    ////////////////////////////////////////////////////////////////////////////////////////
+    ////////                                                                        ////////
+    ////////    Construction of a ParseException when a string can not be parsed    ////////
+    ////////                                                                        ////////
+    ////////////////////////////////////////////////////////////////////////////////////////
+
+    /**
+     * Returns a {@link ParseException} with the specified cause. A localized string
+     * <code>"Error in &lt;{@link #keyword}&gt;"</code> will be prepend to the message.
+     * The error index will be the starting index of this {@code Element}.
+     *
+     * @param  cause   The cause of the failure, or {@code null} if none.
+     * @param  message The message explaining the cause of the failure, or {@code null}
+     *                 for reusing the same message than {@code cause}.
+     * @return The exception to be thrown.
+     */
+    final ParseException parseFailed(final Parser parser, final Exception cause, String message) {
+        if (message == null) {
+            message = Exceptions.getLocalizedMessage(cause, parser.displayLocale);
+        }
+        return (ParseException) new ParseException(Errors.getResources(parser.displayLocale)
+                .getString(Errors.Keys.ErrorIn_2, keyword, message), offset).initCause(cause);
+    }
+
+    /**
+     * Returns a {@link ParseException} with a "Unparsable string" message.
+     * The error message is built from the specified string starting at the specified position.
+     * Properties {@link ParsePosition#getIndex()} and {@link ParsePosition#getErrorIndex()}
+     * must be accurate before this method is invoked.
+     *
+     * @param  text The unparsable string.
+     * @param  position The position in the string.
+     * @return An exception with a formatted error message.
+     */
+    private ParseException unparsableString(final Parser parser, final String text, final ParsePosition position) {
+        final String message;
+        final Errors resources = Errors.getResources(parser.displayLocale);
+        final int errorIndex = Math.max(position.getIndex(), position.getErrorIndex());
+        final int length = text.length();
+        if (errorIndex == length) {
+            message = resources.getString(Errors.Keys.UnexpectedEndOfString_1, keyword);
+        } else {
+            message = resources.getString(Errors.Keys.UnparsableStringInElement_2, keyword,
+                    CharSequences.token(text, errorIndex));
+        }
+        return new ParseException(message, errorIndex);
+    }
+
+    /**
+     * Returns an exception saying that a character is missing.
+     *
+     * @param c The missing character.
+     * @param position The error position.
+     */
+    private ParseException missingCharacter(final Parser parser, final int c, final int position) {
+        final StringBuilder buffer = new StringBuilder(2).appendCodePoint(c);
+        return new ParseException(Errors.getResources(parser.displayLocale)
+                .getString(Errors.Keys.MissingCharacterInElement_2, keyword, buffer), position);
+    }
+
+    /**
+     * Returns an exception saying that a component is missing.
+     *
+     * @param key The name of the missing component.
+     */
+    private ParseException missingParameter(final String key) {
+        int error = offset;
+        if (keyword != null) {
+            error += keyword.length();
+        }
+        return new ParseException(Errors.format(Errors.Keys.MissingComponentInElement_2, keyword, key), error);
+    }
+
+    /**
+     * Returns {@code true} if this element is the root element. For example in a WKT like
+     * {@code "GeodeticCRS["name", Datum["name, ...]]"}, this is true for {@code "GeodeticCRS"}
+     * and false for all other elements inside, like {@code "Datum"}.
+     *
+     * @return {@code true} if this element is the root element.
+     */
+    public boolean isRoot() {
+        return this.offset == 0;
+    }
+
+
+
+
+    //////////////////////////////////////////////////////////////////////////////////////
+    ////////                                                                      ////////
+    ////////    Pull elements from the tree                                       ////////
+    ////////                                                                      ////////
+    //////////////////////////////////////////////////////////////////////////////////////
+
+    /**
+     * Removes the next {@link Date} from the list and returns it.
+     *
+     * @param  key The parameter name. Used for formatting an error message if no date is found.
+     * @return The next {@link Date} on the list.
+     * @throws ParseException if no more date is available.
+     */
+    public Date pullDate(final String key) throws ParseException {
+        final Iterator<Object> iterator = list.iterator();
+        while (iterator.hasNext()) {
+            final Object object = iterator.next();
+            if (object instanceof Date) {
+                iterator.remove();
+                return (Date) object;
+            }
+        }
+        throw missingParameter(key);
+    }
+
+    /**
+     * Removes the next {@link Number} from the list and returns it.
+     *
+     * @param  key The parameter name. Used for formatting an error message if no number is found.
+     * @return The next {@link Number} on the list as a {@code double}.
+     * @throws ParseException if no more number is available.
+     */
+    public double pullDouble(final String key) throws ParseException {
+        final Iterator<Object> iterator = list.iterator();
+        while (iterator.hasNext()) {
+            final Object object = iterator.next();
+            if (object instanceof Number) {
+                iterator.remove();
+                return ((Number) object).doubleValue();
+            }
+        }
+        throw missingParameter(key);
+    }
+
+    /**
+     * Removes the next {@link Number} from the list and returns it as an integer.
+     *
+     * @param  key The parameter name. Used for formatting an error message if no number is found.
+     * @return The next {@link Number} on the list as an {@code int}.
+     * @throws ParseException if no more number is available, or the number is not an integer.
+     */
+    public int pullInteger(final String key) throws ParseException {
+        final Iterator<Object> iterator = list.iterator();
+        while (iterator.hasNext()) {
+            final Object object = iterator.next();
+            if (object instanceof Number) {
+                iterator.remove();
+                final Number number = (Number) object;
+                if (number instanceof Float || number instanceof Double) {
+                    throw new ParseException(Errors.format(
+                            Errors.Keys.UnparsableStringForClass_2, Integer.class, number), offset);
+                }
+                return number.intValue();
+            }
+        }
+        throw missingParameter(key);
+    }
+
+    /**
+     * Removes the next {@link Boolean} from the list and returns it.
+     *
+     * @param  key The parameter name. Used for formatting an error message if no boolean is found.
+     * @return The next {@link Boolean} on the list as a {@code boolean}.
+     * @throws ParseException if no more boolean is available.
+     */
+    public boolean pullBoolean(final String key) throws ParseException {
+        final Iterator<Object> iterator = list.iterator();
+        while (iterator.hasNext()) {
+            final Object object = iterator.next();
+            if (object instanceof Boolean) {
+                iterator.remove();
+                return (Boolean) object;
+            }
+        }
+        throw missingParameter(key);
+    }
+
+    /**
+     * Removes the next {@link String} from the list and returns it.
+     *
+     * @param  key The parameter name. Used for formatting an error message if no number is found.
+     * @return The next {@link String} on the list.
+     * @throws ParseException if no more string is available.
+     */
+    public String pullString(final String key) throws ParseException {
+        final Iterator<Object> iterator = list.iterator();
+        while (iterator.hasNext()) {
+            final Object object = iterator.next();
+            if (object instanceof String) {
+                iterator.remove();
+                return (String) object;
+            }
+        }
+        throw missingParameter(key);
+    }
+
+    /**
+     * Removes the next {@link Object} from the list and returns it.
+     *
+     * @param  key The parameter name. Used for formatting an error message if no number is found.
+     * @return The next {@link Object} on the list (never {@code null}).
+     * @throws ParseException if no more object is available.
+     */
+    public Object pullObject(final String key) throws ParseException {
+        final Iterator<Object> iterator = list.iterator();
+        while (iterator.hasNext()) {
+            final Object object = iterator.next();
+            if (object != null) {
+                iterator.remove();
+                return object;
+            }
+        }
+        throw missingParameter(key);
+    }
+
+    /**
+     * Removes the next {@link Element} from the list and returns it.
+     *
+     * @param  key The element name (e.g. {@code "PRIMEM"}).
+     * @return The next {@link Element} on the list.
+     * @throws ParseException if no more element is available.
+     */
+    public Element pullElement(final String key) throws ParseException {
+        final Element element = pullOptionalElement(key);
+        if (element != null) {
+            return element;
+        }
+        throw missingParameter(key);
+    }
+
+    /**
+     * Removes the next {@link Element} from the list and returns it.
+     *
+     * @param  key The element name (e.g. {@code "PRIMEM"}).
+     * @return The next {@link Element} on the list, or {@code null} if no more element is available.
+     */
+    public Element pullOptionalElement(String key) {
+        key = key.toUpperCase();
+        final Iterator<Object> iterator = list.iterator();
+        while (iterator.hasNext()) {
+            final Object object = iterator.next();
+            if (object instanceof Element) {
+                final Element element = (Element) object;
+                if (element.list!=null && element.keyword.equals(key)) {
+                    iterator.remove();
+                    return element;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Removes and returns the next {@link Element} with no bracket.
+     * The key is used only for only for formatting an error message.
+     *
+     * @param  key The parameter name. Used only for formatting an error message.
+     * @return The next {@link Element} in the list, with no bracket.
+     * @throws ParseException if no more void element is available.
+     */
+    public Element pullVoidElement(final String key) throws ParseException {
+        final Iterator<Object> iterator = list.iterator();
+        while (iterator.hasNext()) {
+            final Object object = iterator.next();
+            if (object instanceof Element) {
+                final Element element = (Element) object;
+                if (element.list == null) {
+                    iterator.remove();
+                    return element;
+                }
+            }
+        }
+        throw missingParameter(key);
+    }
+
+    /**
+     * Returns the next element, or {@code null} if there is no more
+     * element. The element is <strong>not</strong> removed from the list.
+     *
+     * @return The next element, or {@code null} if there is no more elements.
+     */
+    public Object peek() {
+        return list.isEmpty() ? null : list.get(0);
+    }
+
+    /**
+     * Closes this element.
+     *
+     * @throws ParseException If the list still contains some unprocessed elements.
+     */
+    public void close() throws ParseException {
+        if (!isNullOrEmpty(list)) {
+            throw new ParseException(Errors.format(Errors.Keys.UnexpectedParameter_1, list.get(0)), offset + keyword.length());
+        }
+    }
+
+    /**
+     * Returns the keyword. This overriding is needed for correct
+     * formatting of the error message in {@link #close}.
+     */
+    @Override
+    public String toString() {
+        return keyword;
+    }
+
+    /**
+     * Prints this {@code Element} as a tree.
+     * This method is used for debugging purpose only.
+     *
+     * @param out   The output stream.
+     * @param level The indentation level (usually 0).
+     */
+    @Debug
+    public void print(final PrintWriter out, final int level) {
+        final int tabWidth = 4;
+        out.print(CharSequences.spaces(tabWidth * level));
+        out.println(keyword);
+        if (list == null) {
+            return;
+        }
+        final int size = list.size();
+        for (int j=0; j<size; j++) {
+            final Object object = list.get(j);
+            if (object instanceof Element) {
+                ((Element) object).print(out, level+1);
+            } else {
+                out.print(CharSequences.spaces(tabWidth * (level+1)));
+                out.println(object);
+            }
+        }
+    }
+}

Propchange: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/Element.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/Element.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain;charset=UTF-8

Added: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/Parser.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/Parser.java?rev=1680131&view=auto
==============================================================================
--- sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/Parser.java (added)
+++ sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/Parser.java [UTF-8] Mon May 18 23:31:27 2015
@@ -0,0 +1,194 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.sis.io.wkt;
+
+import java.util.Map;
+import java.util.Date;
+import java.util.Locale;
+import java.text.DateFormat;
+import java.text.NumberFormat;
+import java.text.DecimalFormat;
+import java.text.ParsePosition;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import org.apache.sis.util.Workaround;
+
+import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
+
+
+/**
+ * Parses <cite>Well Known Text</cite> (WKT). Parsers are the converse of {@link Formatter}.
+ * Like the later, a parser is constructed with a given set of {@linkplain Symbols symbols}.
+ * Parsers also need a set of factories to be used for instantiating the parsed objects.
+ *
+ * <p>In current version, parsers are usually not intended to be subclassed outside this package.
+ * A few exceptions exist, for example {@link ReferencingParser#alterProperties(Map)} is one of
+ * the few hooks provided for overriding.</p>
+ *
+ * <p>Parsers are not synchronized. It is recommended to create separate parser instances for each thread.
+ * If multiple threads access a parser concurrently, it must be synchronized externally.</p>
+ *
+ * @author  Rémi Eve (IRD)
+ * @author  Martin Desruisseaux (IRD, Geomatys)
+ * @since   0.6
+ * @version 0.6
+ * @module
+ */
+abstract class Parser {
+    /**
+     * Set to {@code true} if parsing of number in scientific notation is allowed.
+     * The way to achieve that is currently a hack, because {@link NumberFormat}
+     * has no API for managing that as of JDK 1.8.
+     *
+     * @todo See if a future version of JDK allows us to get ride of this ugly hack.
+     */
+    @Workaround(library = "JDK", version = "1.8")
+    static final boolean SCIENTIFIC_NOTATION = true;
+
+    /**
+     * The locale for error messages (not for number parsing), or {@code null} for the system default.
+     */
+    final Locale displayLocale;
+
+    /**
+     * The symbols to use for parsing WKT.
+     */
+    final Symbols symbols;
+
+    /**
+     * The symbol for scientific notation, or {@code null} if none.
+     * This is usually {@code "E"} (note the upper case), but could also be something like {@code "×10^"}.
+     */
+    private final String exponentSymbol;
+
+    /**
+     * The object to use for parsing numbers.
+     */
+    private final NumberFormat numberFormat;
+
+    /**
+     * The object to use for parsing dates, created when first needed.
+     */
+    private DateFormat dateFormat;
+
+    /**
+     * Constructs a parser using the specified set of symbols.
+     *
+     * @param symbols The set of symbols to use.
+     * @param displayLocale The locale for error messages (not for number parsing),
+     *        or {@code null} for the system default.
+     */
+    Parser(final Symbols symbols, final Locale displayLocale) {
+        this.displayLocale = displayLocale;
+        ensureNonNull("symbols", symbols);
+        this.symbols = symbols;
+        numberFormat = symbols.createNumberFormat();
+        if (SCIENTIFIC_NOTATION && numberFormat instanceof DecimalFormat) {
+            final DecimalFormat decimalFormat = (DecimalFormat) numberFormat;
+            exponentSymbol = decimalFormat.getDecimalFormatSymbols().getExponentSeparator();
+            String pattern = decimalFormat.toPattern();
+            if (!pattern.contains("E0")) {
+                final StringBuilder buffer = new StringBuilder(pattern);
+                final int split = pattern.indexOf(';');
+                if (split >= 0) {
+                    buffer.insert(split, "E0");
+                }
+                buffer.append("E0");
+                decimalFormat.applyPattern(buffer.toString());
+            }
+        } else {
+            exponentSymbol = null;
+        }
+    }
+
+    /**
+     * Parses a <cite>Well Know Text</cite> (WKT).
+     *
+     * @param  text The text to be parsed.
+     * @param  position The position to start parsing from.
+     * @return The parsed object.
+     * @throws ParseException if the string can not be parsed.
+     */
+    public final Object parseObject(final String text, final ParsePosition position) throws ParseException {
+        final int origin = position.getIndex();
+        try {
+            final Element element = getTree(text, position);
+            final Object object = parse(element);
+            element.close();
+            return object;
+        } catch (ParseException exception) {
+            position.setIndex(origin);
+            if (position.getErrorIndex() < origin) {
+                position.setErrorIndex(exception.getErrorOffset());
+            }
+            throw exception;
+        }
+    }
+
+    /**
+     * Parses the number at the given position.
+     */
+    final Number parseNumber(String text, final ParsePosition position) {
+        /*
+         * HACK: DecimalFormat.parse(…) does not understand lower case 'e' for scientific notation.
+         *       It understands upper case 'E' only, so we need to perform the replacement here.
+         */
+        final int base = position.getIndex();
+        Number number = numberFormat.parse(text, position);
+        if (number != null && exponentSymbol != null) {
+            int i = position.getIndex();
+            if (text.regionMatches(true, i, exponentSymbol, 0, exponentSymbol.length())) {
+                text = new StringBuilder(text).replace(i, i + exponentSymbol.length(), exponentSymbol).toString();
+                position.setIndex(base);
+                number = numberFormat.parse(text, position);
+            }
+        }
+        return number;
+    }
+
+    /**
+     * Parses the date at the given position.
+     */
+    final Date parseDate(final String text, final ParsePosition position) {
+        if (dateFormat == null) {
+            dateFormat = new SimpleDateFormat(WKTFormat.DATE_PATTERN, symbols.getLocale());
+        }
+        return dateFormat.parse(text, position);
+    }
+
+    /**
+     * Parses the next element in the specified <cite>Well Know Text</cite> (WKT) tree.
+     *
+     * @param  element The element to be parsed.
+     * @return The parsed object.
+     * @throws ParseException if the element can not be parsed.
+     */
+    abstract Object parse(final Element element) throws ParseException;
+
+    /**
+     * Returns a tree of {@link Element} for the specified text.
+     *
+     * @param  text       The text to parse.
+     * @param  position   In input, the position where to start parsing from.
+     *                    In output, the first character after the separator.
+     * @return The tree of elements to parse.
+     * @throws ParseException If an parsing error occurred while creating the tree.
+     */
+    final Element getTree(final String text, final ParsePosition position) throws ParseException {
+        return new Element(new Element(this, text, position));
+    }
+}

Propchange: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/Parser.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/Parser.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain;charset=UTF-8

Modified: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/Symbols.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/Symbols.java?rev=1680131&r1=1680130&r2=1680131&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/Symbols.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/Symbols.java [UTF-8] Mon May 18 23:31:27 2015
@@ -66,7 +66,7 @@ import static org.apache.sis.util.Argume
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @since   0.4
- * @version 0.4
+ * @version 0.6
  * @module
  *
  * @see WKTFormat#getSymbols()
@@ -164,6 +164,11 @@ public class Symbols implements Localize
     private String separator;
 
     /**
+     * Same value than {@link #separator} but without leading and trailing spaces.
+     */
+    private transient String trimmedSeparator;
+
+    /**
      * {@code true} if this instance shall be considered as immutable.
      */
     private boolean isImmutable;
@@ -175,13 +180,14 @@ public class Symbols implements Localize
      */
     public Symbols(final Symbols symbols) {
         ensureNonNull("symbols", symbols);
-        locale        = symbols.locale;
-        brackets      = symbols.brackets;
-        quotes        = symbols.quotes;
-        quote         = symbols.quote;
-        openSequence  = symbols.openSequence;
-        closeSequence = symbols.closeSequence;
-        separator     = symbols.separator;
+        locale           = symbols.locale;
+        brackets         = symbols.brackets;
+        quotes           = symbols.quotes;
+        quote            = symbols.quote;
+        openSequence     = symbols.openSequence;
+        closeSequence    = symbols.closeSequence;
+        separator        = symbols.separator;
+        trimmedSeparator = symbols.trimmedSeparator;
     }
 
     /**
@@ -189,14 +195,15 @@ public class Symbols implements Localize
      * The given array is stored by reference - it is not cloned.
      */
     private Symbols(final int[] brackets, final int[] quotes) {
-        this.locale        = Locale.ROOT;
-        this.brackets      = brackets;
-        this.quotes        = quotes;
-        this.quote         = "\"";
-        this.openSequence  = '{';
-        this.closeSequence = '}';
-        this.separator     = ", ";
-        this.isImmutable   = true;
+        this.locale           = Locale.ROOT;
+        this.brackets         = brackets;
+        this.quotes           = quotes;
+        this.quote            = "\"";
+        this.openSequence     = '{';
+        this.closeSequence    = '}';
+        this.separator        = ", ";
+        this.trimmedSeparator = ",";
+        this.isImmutable      = true;
     }
 
     /**
@@ -251,19 +258,26 @@ public class Symbols implements Localize
     }
 
     /**
-     * If the given character is an opening bracket, returns the matching closing bracket.
-     * Otherwise returns -1.
+     * Implementation of {@link #matchingBracket(int)} and {@link #matchingQuote(int)}.
      */
-    final int matchingBracket(final int c) {
-        for (int i=0; i<brackets.length; i+=2) {
-            if (brackets[i] == c) {
-                return brackets[i+1];
+    private static int matching(final int[] chars, final int c) {
+        for (int i = 0; i < chars.length; i += 2) {
+            if (chars[i] == c) {
+                return chars[i + 1];
             }
         }
         return -1;
     }
 
     /**
+     * If the given character is an opening bracket, returns the matching closing bracket.
+     * Otherwise returns -1.
+     */
+    final int matchingBracket(final int c) {
+        return matching(brackets, c);
+    }
+
+    /**
      * Returns the number of paired brackets. For example if the WKT parser accepts both the
      * {@code […]} and {@code (…)} bracket pairs, then this method returns 2.
      *
@@ -324,6 +338,14 @@ public class Symbols implements Localize
     }
 
     /**
+     * If the given character is an opening quote, returns the matching closing quote.
+     * Otherwise returns -1.
+     */
+    final int matchingQuote(final int c) {
+        return matching(quotes, c);
+    }
+
+    /**
      * Returns the number of paired quotes. For example if the WKT parser accepts both the
      * {@code "…"} and {@code “…”} quote pairs, then this method returns 2.
      *
@@ -473,8 +495,17 @@ public class Symbols implements Localize
      */
     public void setSeparator(final String separator) {
         checkWritePermission();
-        ensureNonEmpty("separator", separator);
+        final String s = CharSequences.trimWhitespaces(separator.trim());
+        ensureNonEmpty("separator", s);
         this.separator = separator;
+        trimmedSeparator = s;
+    }
+
+    /**
+     * Returns the separator without trailing spaces.
+     */
+    final String trimmedSeparator() {
+        return trimmedSeparator;
     }
 
     /**
@@ -688,6 +719,7 @@ public class Symbols implements Localize
             if (equals(CURLY_BRACKETS))  return CURLY_BRACKETS;
         }
         quote = String.valueOf(Character.toChars(quotes[1]));
+        trimmedSeparator = CharSequences.trimWhitespaces(separator.trim());
         return this;
     }
 }

Added: sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/io/wkt/ElementTest.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/io/wkt/ElementTest.java?rev=1680131&view=auto
==============================================================================
--- sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/io/wkt/ElementTest.java (added)
+++ sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/io/wkt/ElementTest.java [UTF-8] Mon May 18 23:31:27 2015
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.sis.io.wkt;
+
+import java.util.Locale;
+import java.text.ParsePosition;
+import java.text.ParseException;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+
+/**
+ * Tests the {@link Element} class.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.6
+ * @version 0.6
+ * @module
+ */
+public final strictfp class ElementTest extends TestCase {
+    /**
+     * Tests a {@link Element#pullString(String)}.
+     *
+     * @throws ParseException if an error occurred during the parsing.
+     */
+    @Test
+    public void testString() throws ParseException {
+        final ParsePosition position = new ParsePosition(0);
+        final Element element = new Element(new ParserMock(), "Datum[\"World Geodetic System 1984\"]", position);
+        assertEquals("Datum", element.keyword);
+        assertEquals("World Geodetic System 1984", element.pullString("name"));
+    }
+
+    /**
+     * A dummy parser for testing purpose.
+     */
+    private static final class ParserMock extends Parser {
+        ParserMock() {
+            super(Symbols.SQUARE_BRACKETS, Locale.ENGLISH);
+        }
+
+        @Override
+        Object parse(Element element) throws ParseException {
+            throw new UnsupportedOperationException();
+        }
+    }
+}

Propchange: sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/io/wkt/ElementTest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/io/wkt/ElementTest.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain;charset=UTF-8

Modified: sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/test/suite/MetadataTestSuite.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/test/suite/MetadataTestSuite.java?rev=1680131&r1=1680130&r2=1680131&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/test/suite/MetadataTestSuite.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/test/suite/MetadataTestSuite.java [UTF-8] Mon May 18 23:31:27 2015
@@ -97,6 +97,7 @@ import org.junit.BeforeClass;
     org.apache.sis.io.wkt.CharEncodingTest.class,
     org.apache.sis.io.wkt.ColorsTest.class,
     org.apache.sis.io.wkt.FormatterTest.class,
+    org.apache.sis.io.wkt.ElementTest.class,
     org.apache.sis.io.wkt.WKTFormatTest.class
 })
 public final strictfp class MetadataTestSuite extends TestSuite {

Modified: sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java?rev=1680131&r1=1680130&r2=1680131&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java [UTF-8] Mon May 18 23:31:27 2015
@@ -246,6 +246,11 @@ public final class Errors extends Indexe
         public static final short EmptyProperty_1 = 23;
 
         /**
+         * Error in “{0}”: {1}
+         */
+        public static final short ErrorIn_2 = 190;
+
+        /**
          * Argument ‘{0}’ shall not contain more than {1} elements. A number of {2} is excessive.
          */
         public static final short ExcessiveArgumentSize_3 = 24;
@@ -525,6 +530,16 @@ public final class Errors extends Indexe
         public static final short MissingAuthority_1 = 135;
 
         /**
+         * Missing a ‘{1}’ character in “{0}” element.
+         */
+        public static final short MissingCharacterInElement_2 = 188;
+
+        /**
+         * Missing a “{1}” component in “{0}”.
+         */
+        public static final short MissingComponentInElement_2 = 189;
+
+        /**
          * No horizontal dimension found in “{0}”.
          */
         public static final short MissingHorizontalDimension_1 = 169;
@@ -951,6 +966,11 @@ public final class Errors extends Indexe
         public static final short UnparsableStringForClass_3 = 125;
 
         /**
+         * Can not parse “{1}” in element “{0}”.
+         */
+        public static final short UnparsableStringInElement_2 = 187;
+
+        /**
          * Feature named “{0}” has not yet been resolved.
          */
         public static final short UnresolvedFeatureName_1 = 164;

Modified: sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties?rev=1680131&r1=1680130&r2=1680131&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties [ISO-8859-1] (original)
+++ sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties [ISO-8859-1] Mon May 18 23:31:27 2015
@@ -60,6 +60,7 @@ EmptyArgument_1                   = Argu
 EmptyDictionary                   = The dictionary shall contain at least one entry.
 EmptyEnvelope2D                   = Envelope must be at least two-dimensional and non-empty.
 EmptyProperty_1                   = Property named \u201c{0}\u201d shall not be empty.
+ErrorIn_2                         = Error in \u201c{0}\u201d: {1}
 ExcessiveArgumentSize_3           = Argument \u2018{0}\u2019 shall not contain more than {1} elements. A number of {2} is excessive.
 ExcessiveListSize_2               = A size of {1} elements is excessive for the \u201c{0}\u201d list.
 ExcessiveNumberOfDimensions_1     = For this algorithm, {0} is an excessive number of dimensions.
@@ -115,6 +116,8 @@ MismatchedPropertyType_1          = Mism
 MismatchedTransformDimension_3    = The transform has {2} {0,choice,0#source|1#target} dimension{2,choice,1#|2#s}, while {1} was expected.
 MismatchedValueClass_3            = Class of \u201c{0}\u201d values is \u2018{2}\u2019, but the requested type is \u2018{1}\u2019.
 MissingAuthority_1                = No authority was specified for code \u201c{0}\u201d. The expected syntax is \u201cAUTHORITY:CODE\u201d.
+MissingCharacterInElement_2       = Missing a \u2018{1}\u2019 character in \u201c{0}\u201d element.
+MissingComponentInElement_2       = Missing a \u201c{1}\u201d component in \u201c{0}\u201d.
 MissingInterpolationOrdinates     = Not enough dimension in \u2018MathTransform\u2019 input or output coordinates for the interpolation points.
 MissingNamespace_1                = \u201c{0}\u201d has no namespace.
 MissingRequiredModule_1           = This operation requires the \u201c{0}\u201d module.
@@ -201,6 +204,7 @@ UnmodifiableMetadata              = This
 UnmodifiableObject_1              = This instance of \u2018{0}\u2019 is not modifiable.
 UnparsableStringForClass_2        = Text \u201c{1}\u201d can not be parsed as an object of type \u2018{0}\u2019.
 UnparsableStringForClass_3        = Text \u201c{1}\u201d can not be parsed as an object of type \u2018{0}\u2019, because of the \u201c{2}\u201d characters.
+UnparsableStringInElement_2       = Can not parse \u201c{1}\u201d in element \u201c{0}\u201d.
 UnresolvedFeatureName_1           = Feature named \u201c{0}\u201d has not yet been resolved.
 UnspecifiedCRS                    = Coordinate reference system has not been specified.
 UnspecifiedFormatForClass_1       = No format is specified for objects of class \u2018{0}\u2019.

Modified: sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties?rev=1680131&r1=1680130&r2=1680131&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties [ISO-8859-1] (original)
+++ sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties [ISO-8859-1] Mon May 18 23:31:27 2015
@@ -50,6 +50,7 @@ EmptyArgument_1                   = L\u2
 EmptyDictionary                   = Le dictionnaire doit contenir au moins une entr\u00e9e.
 EmptyEnvelope2D                   = L\u2019enveloppe doit avoir au moins deux dimensions et ne pas \u00eatre vide.
 EmptyProperty_1                   = La propri\u00e9t\u00e9 nomm\u00e9e \u00ab\u202f{0}\u202f\u00bb ne doit pas \u00eatre vide.
+ErrorIn_2                         = Erreur dans \u00ab\u202f{0}\u202f\u00bb\u2008: {1}
 ExcessiveArgumentSize_3           = L\u2019argument \u2018{0}\u2019 ne peut pas contenir plus de {1} \u00e9l\u00e9ments. Un nombre de {2} est excessif.
 ExcessiveListSize_2               = Une taille de {1} \u00e9l\u00e9ments est excessive pour la liste \u00ab\u202f{0}\u202f\u00bb.
 ExcessiveNumberOfDimensions_1     = Pour cet algorithme, {0} est un trop grand nombre de dimensions.
@@ -105,6 +106,8 @@ MismatchedPropertyType_1          = Le t
 MismatchedTransformDimension_3    = La {0,choice,0#source|1#destination} de la transformation a {2} dimension{2,choice,1#|2#s}, alors qu\u2019on en attendait {1}.
 MismatchedValueClass_3            = Les valeurs de \u00ab\u202f{0}\u202f\u00bb sont de la classe \u2018{2}\u2019, alors que le type demand\u00e9 \u00e9tait \u2018{1}\u2019.
 MissingAuthority_1                = Aucune autorit\u00e9 n\u2019a \u00e9t\u00e9 sp\u00e9cifi\u00e9e pour le code \u00ab\u202f{0}\u202f\u00bb. Le format attendu est \u00ab\u202fAUTORIT\u00c9:CODE\u202f\u00bb.
+MissingCharacterInElement_2       = Un caract\u00e8re \u2018{1}\u2019 est manquant dans l\u2019\u00e9l\u00e9ment \u00ab\u202f{0}\u202f\u00bb.
+MissingComponentInElement_2       = Il manque une composante \u00ab\u202f{1}\u202f\u00bb dans l\u2019\u00e9l\u00e9ment \u00ab\u202f{0}\u202f\u00bb.
 MissingInterpolationOrdinates     = La dimension des coordonn\u00e9es en entr\u00e9 ou en sortie du \u2018MathTransform\u2019 n\u2019est pas suffisante pour contenir les points d\u2019interpolation.
 MissingNamespace_1                = \u201c{0}\u201d est d\u00e9fini sans espace de noms.
 MissingRequiredModule_1           = Cette op\u00e9ration requiert le module \u00ab\u202f{0}\u202f\u00bb.
@@ -193,6 +196,7 @@ UnspecifiedFormatForClass_1       = Aucu
 UnspecifiedParameterValues        = Les valeurs des param\u00e8tres n\u2019ont pas \u00e9t\u00e9 sp\u00e9cifi\u00e9es.
 UnparsableStringForClass_2        = Le texte \u00ab\u202f{1}\u202f\u00bb n\u2019est pas reconnu comme un objet de type \u2018{0}\u2019.
 UnparsableStringForClass_3        = Le texte \u00ab\u202f{1}\u202f\u00bb n\u2019est pas reconnu comme un objet de type \u2018{0}\u2019, \u00e0 cause des caract\u00e8res \u00ab\u202f{2}\u202f\u00bb.
+UnparsableStringInElement_2       = Le texte \u00ab\u202f{1}\u202f\u00bb dans l\u2019\u00e9l\u00e9ment \u00ab\u202f{0}\u202f\u00bb ne peut pas \u00eatre lu.
 UnresolvedFeatureName_1           = L\u2019entit\u00e9 nomm\u00e9e \u00ab\u202f{0}\u202f\u00bb n\u2019a pas encore \u00e9t\u00e9 r\u00e9solue.
 UnsupportedImplementation_1       = Les instances de \u2018{0}\u2019 ne peuvent pas \u00eatre g\u00e9r\u00e9es parce que les impl\u00e9mentations arbitraires ne sont pas encore support\u00e9es.
 UnsupportedOperation_1            = L\u2019op\u00e9ration \u2018{0}\u2019 n\u2019est pas support\u00e9e.