private String add()

in endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/MetadataWriter.java [239:527]


    private String add(final Statement stmt, final Object metadata, final Map<Object,String> done,
            final String parent) throws ClassCastException, SQLException, FactoryException
    {
        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 latter will work only for database supporting table inheritance, like PostgreSQL.
         * For other kind of database engine, we cannot 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 equal to `table`.
                 */
                String addTo = table;
                if (helper.dialect.supportsTableInheritance()) {
                    @SuppressWarnings("null")     // `colTables` is initialized in same time as `colTypes`.
                    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
                 * actual value of 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 another 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) {
                            throw new AssertionError(column);                           // Should never happen.
                        }
                    }
                    rt = null;                                                          // For forcing VARCHAR type.
                    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 = Strings.trimOrNull(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 = TableHierarchy.encode(table, 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);
                        assert done.get(value) == dependency;                       // Really identity comparison.
                        if (!helper.dialect.supportsIndexInheritance()) {
                            /*
                             * 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(Reflection.PKTABLE_SCHEM))) &&
                                                (catalog  == null || catalog.equals(rs.getString(Reflection.PKTABLE_CAT))))
                                            {
                                                referencedTables.put(rs.getString(Reflection.FKCOLUMN_NAME),
                                                            new FKey(rs.getString(Reflection.PKTABLE_NAME), null,
                                                                     rs.getString(Reflection.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.forLocale(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(
                        schema(), fkey.tableName, column,       // Source (schema.table.column)
                        target, primaryKey,                     // Target (table.column)
                        !isCodeList));                          // CASCADE if metadata, RESTRICT if CodeList or Enum.
                /*
                 * 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.supportsIndexInheritance() && !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(SQLBuilder.INSERT).appendIdentifier(schema(), table).append(" (").appendIdentifier(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(toStorableValue(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;
    }