Search in sources :

Example 1 with CodeList

use of org.opengis.util.CodeList in project sis by apache.

the class MetadataSource method search.

/**
 * Searches for the given metadata in the database. If such metadata is found, then its
 * identifier (primary key) is returned. Otherwise this method returns {@code null}.
 *
 * @param  table     the table where to search.
 * @param  columns   the table columns as given by {@link #getExistingColumns(String)}, or {@code null}.
 * @param  metadata  a map view of the metadata to search for.
 * @param  stmt      the statement to use for executing the query.
 * @param  helper    an helper class for creating the SQL query.
 * @return the identifier of the given metadata, or {@code null} if none.
 * @throws SQLException if an error occurred while searching in the database.
 */
final String search(final String table, Set<String> columns, final Map<String, Object> metadata, final Statement stmt, final SQLBuilder helper) throws SQLException {
    assert Thread.holdsLock(this);
    helper.clear();
    for (final Map.Entry<String, Object> entry : metadata.entrySet()) {
        /*
             * Gets the value and the column where this value is stored. If the value is non-null,
             * then the column must exist otherwise the metadata will be considered as not found.
             */
        Object value = extractFromCollection(entry.getValue());
        final String column = entry.getKey();
        if (columns == null) {
            columns = getExistingColumns(table);
        }
        if (!columns.contains(column)) {
            if (value != null) {
                // The column was mandatory for the searched metadata.
                return null;
            } else {
                // Do not include a non-existent column in the SQL query.
                continue;
            }
        }
        /*
             * Tests if the value is another metadata, in which case we will invoke this method recursively.
             * Note that if a metadata dependency is not found, we can stop the whole process immediately.
             */
        if (value != null) {
            if (value instanceof CodeList<?>) {
                value = Types.getCodeName((CodeList<?>) value);
            } else if (value instanceof Enum<?>) {
                value = ((Enum<?>) value).name();
            } else {
                String dependency = proxy(value);
                if (dependency != null) {
                    value = dependency;
                } else {
                    final Class<?> type = value.getClass();
                    if (standard.isMetadata(type)) {
                        dependency = search(getTableName(standard.getInterface(type)), null, asValueMap(value), stmt, new SQLBuilder(helper));
                        if (dependency == null) {
                            // Dependency not found.
                            return null;
                        }
                        value = dependency;
                    }
                }
            }
        }
        /*
             * Builds the SQL statement with the resolved value.
             */
        if (helper.isEmpty()) {
            helper.append("SELECT ").append(ID_COLUMN).append(" FROM ").appendIdentifier(schema, table).append(" WHERE ");
        } else {
            helper.append(" AND ");
        }
        helper.appendIdentifier(column).appendCondition(value);
    }
    /*
         * The SQL statement is ready, with metadata dependency (if any) resolved. We can now execute it.
         * If more than one record is found, the identifier of the first one will be selected add a warning
         * will be logged.
         */
    String identifier = null;
    try (ResultSet rs = stmt.executeQuery(helper.toString())) {
        while (rs.next()) {
            final String candidate = rs.getString(1);
            if (candidate != null) {
                if (identifier == null) {
                    identifier = candidate;
                } else if (!identifier.equals(candidate)) {
                    warning(MetadataSource.class, "search", Errors.getResources((Locale) null).getLogRecord(Level.WARNING, Errors.Keys.DuplicatedElement_1, candidate));
                    break;
                }
            }
        }
    }
    return identifier;
}
Also used : Locale(java.util.Locale) CodeList(org.opengis.util.CodeList) SQLBuilder(org.apache.sis.internal.metadata.sql.SQLBuilder) ResultSet(java.sql.ResultSet) Map(java.util.Map) WeakValueHashMap(org.apache.sis.util.collection.WeakValueHashMap) HashMap(java.util.HashMap)

Example 2 with CodeList

use of org.opengis.util.CodeList in project sis by apache.

the class MetadataWriter method add.

/**
 * Implementation of the {@link #add(Object)} method. This method invokes itself recursively,
 * and maintains a map of metadata inserted up to date in order to avoid infinite recursivity.
 *
 * @param  stmt      the statement to use for inserting data.
 * @param  metadata  the metadata object to add.
 * @param  done      the metadata objects already added, mapped to their primary keys.
 * @param  parent    the primary key of the parent, or {@code null} if there is no parent.
 *                   This identifier shall not contain {@linkplain #isReservedChar(int) reserved characters}.
 * @return the identifier (primary key) of the metadata just added.
 * @throws SQLException if an exception occurred while reading or writing the database.
 * @throws ClassCastException if the metadata object does not implement a metadata interface
 *         of the expected package.
 */
private String add(final Statement stmt, final Object metadata, final Map<Object, String> done, final String parent) throws ClassCastException, SQLException {
    final SQLBuilder helper = helper();
    /*
         * Take a snapshot of the metadata content. We do that in order to protect ourself against
         * concurrent changes in the metadata object. This protection is needed because we need to
         * perform multiple passes on the same metadata.
         */
    final Map<String, Object> asValueMap = asValueMap(metadata);
    final Map<String, Object> asSingletons = new LinkedHashMap<>();
    for (final Map.Entry<String, Object> entry : asValueMap.entrySet()) {
        asSingletons.put(entry.getKey(), extractFromCollection(entry.getValue()));
    }
    /*
         * Search the database for an existing metadata.
         */
    final Class<?> implementationType = metadata.getClass();
    final Class<?> interfaceType = standard.getInterface(implementationType);
    final String table = getTableName(interfaceType);
    final Set<String> columns = getExistingColumns(table);
    String identifier = search(table, columns, asSingletons, stmt, helper);
    if (identifier != null) {
        if (done.put(metadata, identifier) != null) {
            throw new AssertionError(metadata);
        }
        return identifier;
    }
    /*
         * Trim the null values or empty collections. We perform this operation only after the check
         * for existing entries, in order to take in account null values when checking existing entries.
         */
    if (columnCreationPolicy != ValueExistencePolicy.ALL) {
        for (final Iterator<Object> it = asSingletons.values().iterator(); it.hasNext(); ) {
            if (it.next() == null) {
                it.remove();
            }
        }
    }
    /*
         * Process to the table creation if it does not already exists. If the table has parents, they will be
         * created first. The later will work only for database supporting table inheritance, like PostgreSQL.
         * For other kind of database engine, we can not store metadata having parent interfaces.
         */
    Boolean isChildTable = createTable(stmt, interfaceType, table, columns);
    if (isChildTable == null) {
        isChildTable = isChildTable(interfaceType);
    }
    /*
         * Add missing columns if there is any. If columns are added, we will keep trace of foreigner keys in
         * this process but will not create the constraints now because the foreigner tables may not exist yet.
         * They will be created later by recursive calls to this method a little bit below.
         */
    Map<String, Class<?>> colTypes = null, colTables = null;
    final Map<String, FKey> foreigners = new LinkedHashMap<>();
    for (final String column : asSingletons.keySet()) {
        if (!columns.contains(column)) {
            if (colTypes == null) {
                colTypes = standard.asTypeMap(implementationType, NAME_POLICY, TypeValuePolicy.ELEMENT_TYPE);
                colTables = standard.asTypeMap(implementationType, NAME_POLICY, TypeValuePolicy.DECLARING_INTERFACE);
            }
            /*
                 * We have found a column to add. Check if the column actually needs to be added to the parent table
                 * (if such parent exists). In most case, the answer is "no" and 'addTo' is equals to 'table'.
                 */
            String addTo = table;
            if (helper.dialect.isTableInheritanceSupported) {
                @SuppressWarnings("null") final Class<?> declaring = colTables.get(column);
                if (!interfaceType.isAssignableFrom(declaring)) {
                    addTo = getTableName(declaring);
                }
            }
            /*
                 * Determine the column data type. We infer that type from the method return value, not from the
                 * actual value for in the given metadata object, since the value type for the same property may
                 * be different in future calls to this method.
                 */
            int maxLength = maximumValueLength;
            Class<?> rt = colTypes.get(column);
            final boolean isCodeList = CodeList.class.isAssignableFrom(rt);
            if (isCodeList || standard.isMetadata(rt)) {
                /*
                     * Found a reference to an other metadata. Remind that column for creating a foreign key
                     * constraint later, except if the return type is an abstract CodeList or Enum (in which
                     * case the reference could be to any CodeList or Enum table). Abstract CodeList or Enum
                     * may happen when the concrete class is not yet available in the GeoAPI version that we
                     * are using.
                     */
                if (!isCodeList || !Modifier.isAbstract(rt.getModifiers())) {
                    if (foreigners.put(column, new FKey(addTo, rt, null)) != null) {
                        // Should never happen.
                        throw new AssertionError(column);
                    }
                }
                // For forcing VARCHAR type.
                rt = null;
                maxLength = maximumIdentifierLength;
            } else if (rt.isEnum()) {
                maxLength = maximumIdentifierLength;
            }
            stmt.executeUpdate(helper.createColumn(schema(), addTo, column, rt, maxLength));
            columns.add(column);
        }
    }
    /*
         * Get the identifier for the new metadata. If no identifier is proposed, we will try to recycle
         * the identifier of the parent.  For example in ISO 19115, Contact (which contains phone number,
         * etc.) is associated only to Responsibility. So it make sense to use the Responsibility ID for
         * the contact info.
         */
    identifier = nonEmpty(removeReservedChars(suggestIdentifier(metadata, asValueMap), null));
    if (identifier == null) {
        identifier = parent;
        if (identifier == null) {
            /*
                 * Arbitrarily pickup the first non-metadata attribute.
                 * Fallback on "unknown" if none are found.
                 */
            identifier = "unknown";
            for (final Object value : asSingletons.values()) {
                if (value != null && !standard.isMetadata(value.getClass())) {
                    identifier = abbreviation(value.toString());
                    break;
                }
            }
        }
    }
    /*
         * If the record to add is located in a child table, we need to prepend the child table name
         * in the identifier in order to allow MetadataSource to locate the right table to query.
         */
    final int minimalIdentifierLength;
    if (isChildTable) {
        identifier = TYPE_OPEN + table + TYPE_CLOSE + identifier;
        minimalIdentifierLength = table.length() + 2;
    } else {
        minimalIdentifierLength = 0;
    }
    /*
         * Check for key collision. We will add a suffix if there is one. Note that the final identifier must be
         * found before we put its value in the map, otherwise cyclic references (if any) will use the wrong value.
         *
         * First, we trim the identifier (primary key) to the maximal length. Then, the loop removes at most four
         * additional characters if the identifier is still too long. After that point, if the identifier still too
         * long, we will let the database driver produces its own SQLException.
         */
    try (IdentifierGenerator idCheck = new IdentifierGenerator(this, schema(), table, ID_COLUMN, helper)) {
        for (int i = 0; i < MINIMAL_LIMIT - 1; i++) {
            final int maxLength = maximumIdentifierLength - i;
            if (maxLength < minimalIdentifierLength)
                break;
            if (identifier.length() > maxLength) {
                identifier = identifier.substring(0, maxLength);
            }
            identifier = idCheck.identifier(identifier);
            if (identifier.length() <= maximumIdentifierLength) {
                break;
            }
        }
    }
    if (done.put(metadata, identifier) != null) {
        throw new AssertionError(metadata);
    }
    /*
         * Process all dependencies now. This block may invoke this method recursively.
         * Once a dependency has been added to the database, the corresponding value in
         * the 'asMap' HashMap is replaced by the identifier of the dependency we just added.
         */
    Map<String, FKey> referencedTables = null;
    for (final Map.Entry<String, Object> entry : asSingletons.entrySet()) {
        Object value = entry.getValue();
        final Class<?> type = value.getClass();
        if (CodeList.class.isAssignableFrom(type)) {
            value = addCode(stmt, (CodeList<?>) value);
        } else if (type.isEnum()) {
            value = ((Enum<?>) value).name();
        } else if (standard.isMetadata(type)) {
            String dependency = proxy(value);
            if (dependency == null) {
                dependency = done.get(value);
                if (dependency == null) {
                    dependency = add(stmt, value, done, identifier);
                    // Really identity comparison.
                    assert done.get(value) == dependency;
                    if (!helper.dialect.isIndexInheritanceSupported) {
                        /*
                             * In a classical object-oriented model, the foreigner key constraints declared in the
                             * parent table would take in account the records in the child table and we would have
                             * nothing special to do. However PostgreSQL 9.1 does not yet inherit index. So if we
                             * detect that a column references some records in two different tables, then we must
                             * suppress the foreigner key constraint.
                             */
                        final String column = entry.getKey();
                        final Class<?> targetType = standard.getInterface(value.getClass());
                        FKey fkey = foreigners.get(column);
                        if (fkey != null && !targetType.isAssignableFrom(fkey.tableType)) {
                            /*
                                 * The foreigner key constraint does not yet exist, so we can
                                 * change the target table. Set the target to the child table.
                                 */
                            fkey.tableType = targetType;
                        }
                        if (fkey == null) {
                            /*
                                 * The foreigner key constraint may already exist. Get a list of all foreigner keys for
                                 * the current table, then verify if the existing constraint references the right table.
                                 */
                            if (referencedTables == null) {
                                referencedTables = new HashMap<>();
                                try (ResultSet rs = stmt.getConnection().getMetaData().getImportedKeys(catalog, schema(), table)) {
                                    while (rs.next()) {
                                        if ((schema() == null || schema().equals(rs.getString("PKTABLE_SCHEM"))) && (catalog == null || catalog.equals(rs.getString("PKTABLE_CAT")))) {
                                            referencedTables.put(rs.getString("FKCOLUMN_NAME"), new FKey(rs.getString("PKTABLE_NAME"), null, rs.getString("FK_NAME")));
                                        }
                                    }
                                }
                            }
                            fkey = referencedTables.remove(column);
                            if (fkey != null && !fkey.tableName.equals(getTableName(targetType))) {
                                /*
                                     * The existing foreigner key constraint doesn't reference the right table.
                                     * We have no other choice than removing it...
                                     */
                                stmt.executeUpdate(helper.clear().append("ALTER TABLE ").appendIdentifier(schema(), table).append(" DROP CONSTRAINT ").appendIdentifier(fkey.keyName).toString());
                                warning(MetadataWriter.class, "add", Messages.getResources(null).getLogRecord(Level.WARNING, Messages.Keys.DroppedForeignerKey_1, table + '.' + column + " ⇒ " + fkey.tableName + '.' + ID_COLUMN));
                            }
                        }
                    }
                }
            }
            value = dependency;
        }
        entry.setValue(value);
    }
    /*
         * Now that all dependencies have been inserted in the database, we can setup the foreigner key constraints
         * if there is any. Note that we deferred the foreigner key creations not because of the missing rows,
         * but because of missing tables (since new tables may be created in the process of inserting dependencies).
         */
    if (!foreigners.isEmpty()) {
        for (final Map.Entry<String, FKey> entry : foreigners.entrySet()) {
            final FKey fkey = entry.getValue();
            Class<?> rt = fkey.tableType;
            final boolean isCodeList = CodeList.class.isAssignableFrom(rt);
            final String primaryKey;
            if (isCodeList) {
                primaryKey = CODE_COLUMN;
            } else {
                primaryKey = ID_COLUMN;
                rt = standard.getInterface(rt);
            }
            final String column = entry.getKey();
            final String target = getTableName(rt);
            stmt.executeUpdate(helper.createForeignKey(// Source (schema.table.column)
            schema(), // Source (schema.table.column)
            fkey.tableName, // Source (schema.table.column)
            column, // Target (table.column)
            target, // Target (table.column)
            primaryKey, // CASCADE if metadata, RESTRICT if CodeList or Enum.
            !isCodeList));
            /*
                 * In a classical object-oriented model, the constraint would be inherited by child tables.
                 * However this is not yet supported as of PostgreSQL 9.6. If inheritance is not supported,
                 * then we have to repeat the constraint creation in child tables.
                 */
            if (!helper.dialect.isIndexInheritanceSupported && !table.equals(fkey.tableName)) {
                stmt.executeUpdate(helper.createForeignKey(schema(), table, column, target, primaryKey, !isCodeList));
            }
        }
    }
    /*
         * Create the SQL statement which will insert the data.
         */
    helper.clear().append("INSERT INTO ").appendIdentifier(schema(), table).append(" (").append(ID_COLUMN);
    for (final String column : asSingletons.keySet()) {
        helper.append(", ").appendIdentifier(column);
    }
    helper.append(") VALUES (").appendValue(identifier);
    for (final Object value : asSingletons.values()) {
        helper.append(", ").appendValue(value);
    }
    final String sql = helper.append(')').toString();
    if (stmt.executeUpdate(sql) != 1) {
        throw new SQLException(Errors.format(Errors.Keys.DatabaseUpdateFailure_3, 0, table, identifier));
    }
    return identifier;
}
Also used : SQLBuilder(org.apache.sis.internal.metadata.sql.SQLBuilder) SQLException(java.sql.SQLException) LinkedHashMap(java.util.LinkedHashMap) CodeList(org.opengis.util.CodeList) ResultSet(java.sql.ResultSet) HashMap(java.util.HashMap) LinkedHashMap(java.util.LinkedHashMap) Map(java.util.Map) IdentityHashMap(java.util.IdentityHashMap)

Example 3 with CodeList

use of org.opengis.util.CodeList in project sis by apache.

the class CodeListSet method contains.

/**
 * Returns {@code true} if this set contains the given element.
 * This methods returns {@code false} if the given argument is {@code null} or
 * is not an instance of the code list class specified at construction time.
 *
 * @param  object  the element to test for presence in this set.
 * @return {@code true} if the given object is contained in this set.
 */
@Override
public boolean contains(final Object object) {
    if (elementType.isInstance(object)) {
        int ordinal = ((CodeList<?>) object).ordinal();
        if (ordinal < Long.SIZE) {
            return (values & (1L << ordinal)) != 0;
        }
        // Rare cases where there is more than 64 elements.
        final BitSet s = supplementary;
        if (s != null) {
            return s.get(ordinal - Long.SIZE);
        }
    }
    return false;
}
Also used : CodeList(org.opengis.util.CodeList) BitSet(java.util.BitSet)

Example 4 with CodeList

use of org.opengis.util.CodeList in project sis by apache.

the class GO_CharacterString method setCodeList.

/**
 * Invoked by JAXB for any XML element which is not a {@code <gco:CharacterString>}, {@code <gcx:FileName>}
 * or {@code <gcx:MimeFileType>}. This method presumes that the element name is the CodeList standard name.
 * If not, the element will be ignored.
 */
@SuppressWarnings({ "unchecked", "unused" })
private void setCodeList(final Object value) {
    final Element e = (Element) value;
    if (e.getNodeType() == Element.ELEMENT_NODE) {
        final Class<?> ct = Types.forStandardName(e.getLocalName());
        final Class<? extends IndexedResourceBundle> resources;
        final short errorKey;
        final Object[] args;
        if (ct != null && CodeList.class.isAssignableFrom(ct)) {
            final String attribute = e.getAttribute("codeListValue").trim();
            if (!attribute.isEmpty()) {
                text = Types.getCodeTitle(Types.forCodeName((Class) ct, attribute, true));
                type = ENUM;
                return;
            } else {
                resources = Errors.class;
                errorKey = Errors.Keys.MissingOrEmptyAttribute_2;
                args = new Object[2];
                args[1] = "codeListValue";
            }
        } else {
            resources = Messages.class;
            errorKey = Messages.Keys.UnknownCodeList_1;
            args = new Object[1];
        }
        args[0] = e.getNodeName();
        Context.warningOccured(Context.current(), GO_CharacterString.class, "setCodeList", resources, errorKey, args);
    }
}
Also used : CodeList(org.opengis.util.CodeList) XmlAnyElement(javax.xml.bind.annotation.XmlAnyElement) JAXBElement(javax.xml.bind.JAXBElement) XmlRootElement(javax.xml.bind.annotation.XmlRootElement) Element(org.w3c.dom.Element) XmlElement(javax.xml.bind.annotation.XmlElement)

Aggregations

CodeList (org.opengis.util.CodeList)4 ResultSet (java.sql.ResultSet)2 HashMap (java.util.HashMap)2 Map (java.util.Map)2 SQLBuilder (org.apache.sis.internal.metadata.sql.SQLBuilder)2 SQLException (java.sql.SQLException)1 BitSet (java.util.BitSet)1 IdentityHashMap (java.util.IdentityHashMap)1 LinkedHashMap (java.util.LinkedHashMap)1 Locale (java.util.Locale)1 JAXBElement (javax.xml.bind.JAXBElement)1 XmlAnyElement (javax.xml.bind.annotation.XmlAnyElement)1 XmlElement (javax.xml.bind.annotation.XmlElement)1 XmlRootElement (javax.xml.bind.annotation.XmlRootElement)1 WeakValueHashMap (org.apache.sis.util.collection.WeakValueHashMap)1 Element (org.w3c.dom.Element)1