001/*
002 *    GeoAPI - Java interfaces for OGC/ISO standards
003 *    http://www.geoapi.org
004 *
005 *    Copyright (C) 2018-2019 Open Geospatial Consortium, Inc.
006 *    All Rights Reserved. http://www.opengeospatial.org/ogc/legal
007 *
008 *    Permission to use, copy, and modify this software and its documentation, with
009 *    or without modification, for any purpose and without fee or royalty is hereby
010 *    granted, provided that you include the following on ALL copies of the software
011 *    and documentation or portions thereof, including modifications, that you make:
012 *
013 *    1. The full text of this NOTICE in a location viewable to users of the
014 *       redistributed or derivative work.
015 *    2. Notice of any changes or modifications to the OGC files, including the
016 *       date changes were made.
017 *
018 *    THIS SOFTWARE AND DOCUMENTATION IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE
019 *    NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
020 *    TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT
021 *    THE USE OF THE SOFTWARE OR DOCUMENTATION WILL NOT INFRINGE ANY THIRD PARTY
022 *    PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS.
023 *
024 *    COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR
025 *    CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENTATION.
026 *
027 *    The name and trademarks of copyright holders may NOT be used in advertising or
028 *    publicity pertaining to the software without specific, written prior permission.
029 *    Title to copyright in this software and any associated documentation will at all
030 *    times remain with copyright holders.
031 */
032package org.opengis.geoapi;
033
034import java.util.Map;
035import java.util.HashMap;
036
037
038/**
039 * Departures in GeoAPI interfaces compared to OGC/ISO schemas.
040 * Each {@code Departures} instance is initialized with a default set of departure information.
041 * More departure can be added by calls to {@code add(…)} methods.
042 *
043 * @author  Martin Desruisseaux (Geomatys)
044 * @since   3.1
045 * @version 3.1
046 */
047public class Departures {
048    /**
049     * ISO 19115-2 classes merged with ISO 19115-1 classes. For example ISO 19115-2 defines {@code MI_Band}
050     * as an extension of ISO 19115-1 {@code MD_Band}, but GeoAPI merges those two types in a single interface
051     * for simplicity.
052     *
053     * <p>Keys or extension types (e.g. the {@code "MI_*"} types defined by the metadata extension for imagery)
054     * and values are the base types in which the extension has been merged (e.g. the {@code "MD_*"} types defined
055     * by metadata fundamentals).</p>
056     */
057    private final Map<String,String> mergedTypes;
058
059    /**
060     * Changes in the spelling of an identifier. The differences may be for historical reasons,
061     * or in a few cases because of a misspelling in the XSD file compared to the UML.
062     *
063     * <p>Keys are the spellings used in GeoAPI {@link org.opengis.annotation.UML} annotations
064     * and values are the spellings used in XSD files.</p>
065     */
066    final Map<String,String> spellingChanges;
067
068    /**
069     * Creates new collections of departure information. All maps in public fields ({@link #mergedTypes},
070     * <i>etc.</i>) are initialized with new instances and populated.
071     */
072    public Departures() {
073        Map<String,String> m = new HashMap<>(12);
074        // ………Merge what…………………………………………………………Into……………………………………………
075        m.put("MI_Band_Type",                 "MD_Band_Type");
076        m.put("MI_CoverageDescription_Type",  "MD_CoverageDescription_Type");
077        m.put("MI_Georectified_Type",         "MD_Georectified_Type");
078        m.put("MI_Georeferenceable_Type",     "MD_Georeferenceable_Type");
079        m.put("LE_Source_Type",               "LI_Source_Type");
080        m.put("LE_ProcessStep_Type",          "LI_ProcessStep_Type");
081        m.put("AbstractMX_File_Type",         "MX_DataFile_Type");
082        m.put("Abstract_DataQuality_Type",    "DQ_DataQuality_Type");
083        m.put("Abstract_QualityElement_Type", "AbstractDQ_Element_Type");
084        mergedTypes = m;
085
086        m = new HashMap<>(12);
087        m.put("MI_EnvironmentalRecord.meteorologicalConditions", "meterologicalConditions");    // Misspelling in ISO 19115-3:2016
088        m.put("MI_Requirement.satisfiedPlan",                    "satisifiedPlan");             // Misspelling in ISO 19115-3:2016
089        m.put("LI_ProcessStep.stepDateTime",                     "dateTime");                   // Spelling change in XSD files
090        m.put("DQ_Result.valueType",                             "valueRecordType");            // TODO: verify in ISO 19157
091        spellingChanges = m;
092    }
093
094    /**
095     * Adds a class to be retrofitted into another class. For example ISO 19115-2 defines {@code MI_Band} as
096     * an extension of ISO 19115-1 {@code MD_Band}, but GeoAPI merges those two types in a single interface
097     * for simplicity.
098     *
099     * @param  toRetrofit  name of the type to retrofit into another type. Example: {@code MI_Band}.
100     * @param  target      name of a type which will receive the properties of the retrofitted type.
101     *                     Example: {@code MD_Band}.
102     */
103    public void addMergedType(final String toRetrofit, final String target) {
104        if (mergedTypes.putIfAbsent(toRetrofit, target) != null) {
105            throw new IllegalArgumentException(toRetrofit + " is already retrofitted.");
106        }
107    }
108
109    /**
110     * Changes the spelling of an identifier. The differences may be for historical reasons,
111     * or in a few cases because of a misspelling in the XSD file compared to the UML.
112     *
113     * @param  uml  the spelling in the UML, including class name. Example: {@code "LI_ProcessStep.stepDateTime"}.
114     * @param  xsd  the spelling in the XSD file. Example: {@code "dateTime"}.
115     */
116    public void addSpellingChange(final String uml, final String xsd) {
117        if (spellingChanges.put(uml, xsd) != null) {
118            throw new IllegalArgumentException("A spelling change is already declared for " + uml);
119        }
120    }
121
122    /**
123     * Returns the name of a class merging the given class and its (usually) parent class.
124     * For example {@code "MI_Band_Type"} is renamed as {@code "MD_Band_Type"}.
125     * We do that because we use only one class for representing those two distinct ISO types.
126     * Note that not all ISO 19115-2 types extend an ISO 19115-1 type, so we need to apply a case-by-case approach.
127     * If there is no merge to apply, then this method returns the given name unchanged.
128     *
129     * @param  name  name of a class to potentially merge with its parent class.
130     * @return the merged class name (may be the given name) together with other information.
131     */
132    final MergeInfo nameOfMergedType(final String name) {
133        String target = mergedTypes.remove(name);
134        if (target == null) {
135            return new MergeInfo(name, false);
136        } else {
137            return new MergeInfo(target, name.startsWith("Abstract"));
138        }
139    }
140
141    /**
142     * Information about a type that may have been retrofitted into another type.
143     * For example ISO 19115-2 defines {@code MI_Band} as an extension of ISO 19115-1 {@code MD_Band},
144     * but GeoAPI merges those two types in a single interface for simplicity. Sometime the merge also
145     * implies to change properties order.
146     */
147    static final class MergeInfo {
148        /**
149         * Name of the merged type.
150         */
151        final String typeName;
152
153        /**
154         * Whether we will need to reorder properties. Reordering will be needed if the properties of
155         * parent type are retrofitted into the properties of the child type instead than the converse.
156         * We identifies this situation by the {@code "Abstract"} prefix in type to retrofit.
157         */
158        private final boolean needToReorderProperties;
159
160        /**
161         * Names of properties to keep last, or {@code null} if none.
162         * This is set to a non-null value only if {@link #needToReorderProperties} is {@code true}.
163         */
164        private String[] propertiesToKeepLast;
165
166        /**
167         * Creates information for a type.
168         */
169        private MergeInfo(final String typeName, final boolean reorder) {
170            this.typeName = typeName;
171            needToReorderProperties = reorder;
172        }
173
174        /**
175         * Invoked before properties are added in the given map. This method does nothing
176         * in the common case where there is no merge operation to prepare.
177         */
178        final void beforeAddProperties(final Map<String,?> properties) {
179            if (needToReorderProperties) {
180                propertiesToKeepLast = properties.keySet().toArray(new String[properties.size()]);
181            }
182        }
183
184        /**
185         * Invoked after properties are added in the given map. If a merge operation has been applied,
186         * then this method may reorder entries in the given map by moving last the properties recorded
187         * in this {@code MergeInfo}.
188         */
189        final <E> void afterAddProperties(final Map<String,E> properties) {
190            if (propertiesToKeepLast != null) {
191                for (final String p : propertiesToKeepLast) {
192                    if (p != null) {
193                        final E e = properties.remove(p);
194                        if (e == null) {
195                            throw new IllegalArgumentException("Missing property for " + p);
196                        }
197                        properties.put(p, e);
198                    }
199                }
200            }
201        }
202    }
203}