001/*
002 *    GeoAPI - Java interfaces for OGC/ISO standards
003 *    Copyright © 2008-2024 Open Geospatial Consortium, Inc.
004 *    http://www.geoapi.org
005 *
006 *    Licensed under the Apache License, Version 2.0 (the "License");
007 *    you may not use this file except in compliance with the License.
008 *    You may obtain a copy of the License at
009 *
010 *        http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *    Unless required by applicable law or agreed to in writing, software
013 *    distributed under the License is distributed on an "AS IS" BASIS,
014 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 *    See the License for the specific language governing permissions and
016 *    limitations under the License.
017 */
018package org.opengis.test.util;
019
020import java.util.List;
021import org.opengis.util.*;
022import org.opengis.test.Validator;
023import org.opengis.test.ValidatorContainer;
024import static org.junit.jupiter.api.Assertions.*;
025
026
027/**
028 * Validates {@link GenericName} and related objects from the {@code org.opengis.util} package.
029 *
030 * <p>This class is provided for users wanting to override the validation methods. When the default
031 * behavior is sufficient, the {@link org.opengis.test.Validators} static methods provide a more
032 * convenient way to validate various kinds of objects.</p>
033 *
034 * @author  Martin Desruisseaux (Geomatys)
035 * @version 3.1
036 * @since   2.2
037 */
038public class NameValidator extends Validator {
039    /**
040     * Creates a new validator instance.
041     *
042     * @param container  the set of validators to use for validating other kinds of objects
043     *                   (see {@linkplain #container field javadoc}).
044     */
045    public NameValidator(final ValidatorContainer container) {
046        super(container, "org.opengis.util");
047    }
048
049    /**
050     * Ensures that the {@link CharSequence} methods are consistent with the {@code toString()} value.
051     *
052     * @param  object  the object to validate, or {@code null}.
053     */
054    public void validate(final InternationalString object) {
055        if (object == null) {
056            return;
057        }
058        final int length = object.length();
059        final String s = object.toString();
060        mandatory(s, "CharSequence: toString() shall never returns null.");
061        if (s != null) {
062            assertEquals(s.length(), length, "CharSequence: length is inconsistent with toString() length.");
063            boolean expectLowSurrogate = false;
064            for (int i=0; i<length; i++) {
065                final char c = s.charAt(i);
066                assertEquals(c, object.charAt(i), "CharSequence: character inconsistent with toString().");
067                if (expectLowSurrogate) {
068                    assertTrue(Character.isLowSurrogate(c), "CharSequence: High surrogate shall be followed by low surrogate.");
069                }
070                expectLowSurrogate = Character.isHighSurrogate(c);
071            }
072            assertFalse(expectLowSurrogate, "CharSequence: High surrogate shall be followed by low surrogate.");
073        }
074        mandatory(object.toString(null), "InternationalString: toString(Locale) shall not return null.");
075        assertEquals(object, object, "InternationalString: shall be equal to itself.");
076        assertEquals(0, object.compareTo(object), "InternationalString: shall be comparable to itself.");
077    }
078
079    /**
080     * Ensures that ISO 19103 or GeoAPI restrictions apply.
081     *
082     * @param  object  the object to validate, or {@code null}.
083     */
084    public void validate(final NameSpace object) {
085        if (object == null) {
086            return;
087        }
088        final GenericName name = object.name();
089        mandatory(name, "NameSpace: shall have a name.");
090        if (name != null) {
091            final NameSpace scope = name.scope();
092            mandatory(scope, "NameSpace: identifier shall have a global scope.");
093            if (scope != null) {
094                assertTrue(scope.isGlobal(), "NameSpace: identifier scope shall be global.");
095            }
096            /*
097             * Following test is a consequence of the previous one, so we check the scope first in
098             * order to report the error as a bad scope before to reference this GeoAPI extension.
099             */
100            assertSame(name, name.toFullyQualifiedName(), "NameSpace: the identifier shall be fully qualified.");
101            /*
102             * Do not validate global namespaces because their name could be anything including
103             * an empty name, and the 'validate' method below does not accept empty collections.
104             */
105            if (!object.isGlobal()) {
106                validate(name, name.getParsedNames());
107            }
108        }
109    }
110
111    /**
112     * For each interface implemented by the given object, invokes the corresponding
113     * {@code validate(…)} method defined in this class (if any).
114     *
115     * @param  object  the object to dispatch to {@code validate(…)} methods, or {@code null}.
116     * @return number of {@code validate(…)} methods invoked in this class for the given object.
117     */
118    public int dispatch(final GenericName object) {
119        int n = 0;
120        if (object != null) {
121            if (object instanceof LocalName)  {validate((LocalName)  object); n++;}
122            if (object instanceof ScopedName) {validate((ScopedName) object); n++;}
123        }
124        return n;
125    }
126
127    /**
128     * Performs some tests that are common to all subclasses of {@link GenericName}. This method
129     * shall not invokes {@link #validate(LocalName)} or {@link #validate(ScopedName)} in order
130     * to avoid never-ending loop.
131     *
132     * <p>This method shall not validate the scope, since it could leads to a never-ending loop.</p>
133     *
134     * @param object       the name to validate.
135     * @param parsedNames  expected parsed names.
136     */
137    private void validate(final GenericName object, final List<? extends LocalName> parsedNames) {
138        mandatory(parsedNames, "GenericName: getParsedNames() shall not return null.");
139        if (parsedNames != null) {
140            validate(parsedNames);
141            assertFalse(parsedNames.isEmpty(),
142                    "GenericName: getParsedNames() shall not return an empty list.");
143            final int size = parsedNames.size();
144            assertEquals(size, object.depth(),
145                    "GenericName: getParsedNames() list size shall be equal to depth().");
146            assertEquals(parsedNames.get(0), object.head(),
147                    "GenericName: head() shall be the first element in getParsedNames() list.");
148            assertEquals(parsedNames.get(size-1), object.tip(),
149                    "GenericName: tip() shall be the last element in getParsedNames() list.");
150        }
151        /*
152         * Validates fully qualified name.
153         */
154        final GenericName fullyQualified = object.toFullyQualifiedName();
155        mandatory(fullyQualified, "GenericName: toFullyQualifiedName() shall not return null.");
156        if (fullyQualified != null) {
157            assertEquals(object.scope().isGlobal(), fullyQualified == object,
158                    "GenericName: toFullyQualifiedName() inconsistent with the global scope status.");
159        }
160        /*
161         * Validates string representations.
162         */
163        final String unlocalized = object.toString();
164        mandatory(unlocalized, "GenericName: toString() shall never returns null.");
165        if (unlocalized != null && fullyQualified != null) {
166            assertTrue(fullyQualified.toString().endsWith(unlocalized),
167                    "GenericName: fully qualified name shall end with the name.");
168        }
169        final InternationalString localized = object.toInternationalString();
170        validate(localized);
171        if (localized != null && fullyQualified != null) {
172            assertTrue(fullyQualified.toInternationalString().toString().endsWith(localized.toString()),
173                    "GenericName: fully qualified name shall end with the name (localized version).");
174        }
175        /*
176         * Validates comparisons.
177         */
178        assertEquals(object, object, "GenericName: shall be equal to itself.");
179        assertEquals(0, object.compareTo(object), "GenericName: shall be comparable to itself.");
180    }
181
182    /**
183     * Ensures that ISO 19103 or GeoAPI restrictions apply.
184     *
185     * @param  object  the object to validate, or {@code null}.
186     */
187    public void validate(final LocalName object) {
188        if (object == null) {
189            return;
190        }
191        validate(object.scope());
192        final List<? extends LocalName> parsedNames = object.getParsedNames();
193        validate(object, parsedNames);
194        if (parsedNames != null) {
195            assertEquals(1, parsedNames.size(), "LocalName: shall have exactly one parsed name.");
196            assertSame(object, parsedNames.get(0),
197                    "LocalName: the parsed name element shall be the enclosing local name.");
198        }
199    }
200
201    /**
202     * Ensures that ISO 19103 or GeoAPI restrictions apply.
203     *
204     * @param  object  the object to validate, or {@code null}.
205     */
206    public void validate(final ScopedName object) {
207        if (object == null) {
208            return;
209        }
210        final List<? extends LocalName> parsedNames = object.getParsedNames();
211        validate(object, parsedNames);
212        final NameSpace scope = object.scope();
213        validate(scope);
214        if (scope != null) {
215            assertEquals(scope, object.head().scope(), "ScopedName: head.scope shall be equal to the scope.");
216        }
217        if (parsedNames != null) {
218            boolean global = (scope != null) && scope.isGlobal();
219            for (final LocalName name : parsedNames) {
220                assertNotNull(name, "ScopedName: getParsedNames() cannot contain null element.");
221                assertNotSame(object, name, "ScopedName: the enclosing scoped name cannot be in any parsed name.");
222                assertEquals(global, name.scope().isGlobal(), "ScopedName: inconsistent value of isGlobal().");
223                global = false;         // Only the first name may be global.
224                validate(name);
225            }
226        }
227        /*
228         * Validates tail.
229         */
230        final int depth = object.depth();
231        final GenericName tail = object.tail();
232        mandatory(tail, "ScopedName: tail() shall not return null.");
233        if (tail != null) {
234            assertEquals(depth-1, tail.depth(),
235                    "ScopedName: tail() shall have one less element than the enclosing scoped name.");
236            assertEquals(object.tip(), tail.tip(),
237                    "ScopedName: tip().toString() and tail.tip().toString() shall be equal.");
238            if (parsedNames != null) {
239                assertEquals(parsedNames.subList(1, depth), tail.getParsedNames(),
240                        "ScopedName: tail() shall be defined as subList(1, depth).");
241            }
242        }
243        /*
244         * Validates path.
245         */
246        final GenericName path = object.path();
247        mandatory(path, "ScopedName: the path shall not be null.");
248        if (path != null) {
249            assertEquals(depth-1, path.depth(),
250                    "ScopedName: path() shall have one less element than the enclosing scoped name.");
251            assertEquals(object.head(), path.head(),
252                    "ScopedName: head() and path.head() shall be equal.");
253            if (parsedNames != null) {
254                assertEquals(parsedNames.subList(0, depth-1), path.getParsedNames(),
255                        "ScopedName: path() shall be defined as subList(0, depth-1).");
256            }
257        }
258    }
259}