/**
   * Method to add a multi-tenancy discriminator column.
   *
   * @param colmd Metadata defining the column required
   */
  protected void addMultitenancyMapping(ColumnMetaData colmd) {
    String colName = "TENANT_ID";
    if (colmd != null && colmd.getName() != null) {
      colName = colmd.getName();
    }
    String typeName = String.class.getName();
    if (colmd != null && colmd.getJdbcType() != null) {
      if (colmd.getJdbcType() == JdbcType.INTEGER) {
        typeName = Integer.class.getName();
      }
    }

    if (typeName.equals(Integer.class.getName())) {
      tenantMapping = new IntegerMapping();
    } else {
      tenantMapping = new StringMapping();
    }
    tenantMapping.setTable(this);
    tenantMapping.initialize(storeMgr, typeName);
    Column tenantColumn =
        addColumn(
            typeName,
            storeMgr.getIdentifierFactory().newIdentifier(IdentifierType.COLUMN, colName),
            tenantMapping,
            colmd);
    storeMgr.getMappingManager().createDatastoreMapping(tenantMapping, tenantColumn, typeName);
    logMapping("MULTITENANCY", tenantMapping);
  }
  /**
   * Method to initialise the table definition.
   *
   * @param clr The ClassLoaderResolver
   */
  public void initialize(ClassLoaderResolver clr) {
    assertIsUninitialized();

    MapMetaData mapmd = mmd.getMap();
    if (mapmd == null) {
      throw new NucleusUserException(Localiser.msg("057017", mmd));
    }

    PrimaryKeyMetaData pkmd =
        (mmd.getJoinMetaData() != null ? mmd.getJoinMetaData().getPrimaryKeyMetaData() : null);
    boolean pkColsSpecified = (pkmd != null && pkmd.getColumnMetaData() != null);
    boolean pkRequired = requiresPrimaryKey();

    // Add owner mapping
    ColumnMetaData[] ownerColmd = null;
    if (mmd.getJoinMetaData() != null
        && mmd.getJoinMetaData().getColumnMetaData() != null
        && mmd.getJoinMetaData().getColumnMetaData().length > 0) {
      // Column mappings defined at this side (1-N, M-N)
      // When specified at this side they use the <join> tag
      ownerColmd = mmd.getJoinMetaData().getColumnMetaData();
    }
    ownerMapping =
        ColumnCreator.createColumnsForJoinTables(
            clr.classForName(ownerType),
            mmd,
            ownerColmd,
            storeMgr,
            this,
            pkRequired,
            false,
            FieldRole.ROLE_OWNER,
            clr);
    if (NucleusLogger.DATASTORE.isDebugEnabled()) {
      logMapping(mmd.getFullFieldName() + ".[OWNER]", ownerMapping);
    }

    String keyValueFieldName =
        (mmd.getKeyMetaData() != null ? mmd.getKeyMetaData().getMappedBy() : null);
    String valueKeyFieldName =
        (mmd.getValueMetaData() != null ? mmd.getValueMetaData().getMappedBy() : null);

    // Add key mapping
    boolean keyPC = (mmd.hasMap() && mmd.getMap().keyIsPersistent());
    Class keyCls = clr.classForName(mapmd.getKeyType());
    if (keyValueFieldName != null && isEmbeddedValuePC()) {
      // Added in value code
    } else if (isSerialisedKey()
        || isEmbeddedKeyPC()
        || (isEmbeddedKey() && !keyPC)
        || ClassUtils.isReferenceType(keyCls)) {
      // Key = PC(embedded), PC(serialised), Non-PC(serialised), Non-PC(embedded), Reference
      keyMapping = storeMgr.getMappingManager().getMapping(this, mmd, clr, FieldRole.ROLE_MAP_KEY);
      if (Boolean.TRUE.equals(mmd.getContainer().allowNulls())) {
        // Make all key col(s) nullable so we can store null elements
        for (int i = 0; i < keyMapping.getNumberOfDatastoreMappings(); i++) {
          Column elementCol = keyMapping.getDatastoreMapping(i).getColumn();
          elementCol.setNullable(true);
        }
      }
      if (NucleusLogger.DATASTORE.isDebugEnabled()) {
        logMapping(mmd.getFullFieldName() + ".[KEY]", keyMapping);
      }
      if (valueKeyFieldName != null && isEmbeddedKeyPC()) {
        // Key (PC) is embedded and value is a field of the key
        EmbeddedKeyPCMapping embMapping = (EmbeddedKeyPCMapping) keyMapping;
        valueMapping = embMapping.getJavaTypeMapping(valueKeyFieldName);
      }
    } else {
      // Key = PC
      ColumnMetaData[] keyColmd = null;
      KeyMetaData keymd = mmd.getKeyMetaData();
      if (keymd != null
          && keymd.getColumnMetaData() != null
          && keymd.getColumnMetaData().length > 0) {
        // Column mappings defined at this side (1-N, M-N)
        keyColmd = keymd.getColumnMetaData();
      }
      keyMapping =
          ColumnCreator.createColumnsForJoinTables(
              keyCls, mmd, keyColmd, storeMgr, this, false, false, FieldRole.ROLE_MAP_KEY, clr);
      if (mmd.getContainer().allowNulls() == Boolean.TRUE) {
        // Make all key col(s) nullable so we can store null elements
        for (int i = 0; i < keyMapping.getNumberOfDatastoreMappings(); i++) {
          Column elementCol = keyMapping.getDatastoreMapping(i).getColumn();
          elementCol.setNullable(true);
        }
      }
      if (NucleusLogger.DATASTORE.isDebugEnabled()) {
        logMapping(mmd.getFullFieldName() + ".[KEY]", keyMapping);
      }
    }

    // Add value mapping
    boolean valuePC = (mmd.hasMap() && mmd.getMap().valueIsPersistent());
    Class valueCls = clr.classForName(mapmd.getValueType());
    if (valueKeyFieldName != null && isEmbeddedKeyPC()) {
      // Added in key code
    } else if (isSerialisedValue()
        || isEmbeddedValuePC()
        || (isEmbeddedValue() && !valuePC)
        || ClassUtils.isReferenceType(valueCls)) {
      // Value = PC(embedded), PC(serialised), Non-PC(serialised), Non-PC(embedded), Reference
      valueMapping =
          storeMgr.getMappingManager().getMapping(this, mmd, clr, FieldRole.ROLE_MAP_VALUE);
      if (mmd.getContainer().allowNulls() == Boolean.TRUE) {
        // Make all value col(s) nullable so we can store null elements
        for (int i = 0; i < valueMapping.getNumberOfDatastoreMappings(); i++) {
          Column elementCol = valueMapping.getDatastoreMapping(i).getColumn();
          elementCol.setNullable(true);
        }
      }
      if (NucleusLogger.DATASTORE.isDebugEnabled()) {
        logMapping(mmd.getFullFieldName() + ".[VALUE]", valueMapping);
      }
      if (keyValueFieldName != null && isEmbeddedValuePC()) {
        // Value (PC) is embedded and key is a field of the value
        EmbeddedValuePCMapping embMapping = (EmbeddedValuePCMapping) valueMapping;
        keyMapping = embMapping.getJavaTypeMapping(keyValueFieldName);
      }
    } else {
      // Value = PC
      ColumnMetaData[] valueColmd = null;
      ValueMetaData valuemd = mmd.getValueMetaData();
      if (valuemd != null
          && valuemd.getColumnMetaData() != null
          && valuemd.getColumnMetaData().length > 0) {
        // Column mappings defined at this side (1-N, M-N)
        valueColmd = valuemd.getColumnMetaData();
      }
      valueMapping =
          ColumnCreator.createColumnsForJoinTables(
              clr.classForName(mapmd.getValueType()),
              mmd,
              valueColmd,
              storeMgr,
              this,
              false,
              true,
              FieldRole.ROLE_MAP_VALUE,
              clr);
      if (mmd.getContainer().allowNulls() == Boolean.TRUE) {
        // Make all value col(s) nullable so we can store null elements
        for (int i = 0; i < valueMapping.getNumberOfDatastoreMappings(); i++) {
          Column elementCol = valueMapping.getDatastoreMapping(i).getColumn();
          elementCol.setNullable(true);
        }
      }
      if (NucleusLogger.DATASTORE.isDebugEnabled()) {
        logMapping(mmd.getFullFieldName() + ".[VALUE]", valueMapping);
      }
    }

    // Add order mapping if required
    boolean orderRequired = false;
    if (mmd.getOrderMetaData() != null) {
      // User requested order column so add one
      orderRequired = true;
    } else if (requiresPrimaryKey() && !pkColsSpecified) {
      // PK is required so maybe need to add an index to form the PK
      if (isEmbeddedKeyPC()) {
        if (mmd.getMap().getKeyClassMetaData(clr, storeMgr.getMetaDataManager()).getIdentityType()
            != IdentityType.APPLICATION) {
          // Embedded key PC with datastore id so we need an index to form the PK
          orderRequired = true;
        }
      } else if (isSerialisedKey()) {
        // Serialised key, so need an index to form the PK
        orderRequired = true;
      } else if (keyMapping instanceof ReferenceMapping) {
        // ReferenceMapping, so have order if more than 1 implementation
        ReferenceMapping refMapping = (ReferenceMapping) keyMapping;
        if (refMapping.getJavaTypeMapping().length > 1) {
          orderRequired = true;
        }
      } else if (!(keyMapping instanceof PersistableMapping)) {
        // Non-PC, so depends if the key column can be used as part of a PK
        // TODO This assumes the keyMapping has a single column but what if it is Color with 4 cols?
        Column elementCol = keyMapping.getDatastoreMapping(0).getColumn();
        if (!storeMgr.getDatastoreAdapter().isValidPrimaryKeyType(elementCol.getJdbcType())) {
          // Not possible to use this Non-PC type as part of the PK
          orderRequired = true;
        }
      }
    }
    if (orderRequired) {
      // Order/Adapter (index) column is required (integer based)
      ColumnMetaData orderColmd = null;
      if (mmd.getOrderMetaData() != null
          && mmd.getOrderMetaData().getColumnMetaData() != null
          && mmd.getOrderMetaData().getColumnMetaData().length > 0) {
        // Specified "order" column info
        orderColmd = mmd.getOrderMetaData().getColumnMetaData()[0];
        if (orderColmd.getName() == null) {
          // No column name so use default
          orderColmd = new ColumnMetaData(orderColmd);
          DatastoreIdentifier id = storeMgr.getIdentifierFactory().newIndexFieldIdentifier(mmd);
          orderColmd.setName(id.getName());
        }
      } else {
        // No column name so use default
        DatastoreIdentifier id = storeMgr.getIdentifierFactory().newIndexFieldIdentifier(mmd);
        orderColmd = new ColumnMetaData();
        orderColmd.setName(id.getName());
      }
      orderMapping =
          storeMgr
              .getMappingManager()
              .getMapping(int.class); // JDO2 spec [18.5] order column is assumed to be "int"
      ColumnCreator.createIndexColumn(
          orderMapping, storeMgr, clr, this, orderColmd, pkRequired && !pkColsSpecified);
      if (NucleusLogger.DATASTORE.isDebugEnabled()) {
        logMapping(mmd.getFullFieldName() + ".[ORDER]", orderMapping);
      }
    }

    // Define primary key of the join table (if any)
    if (pkRequired) {
      if (pkColsSpecified) {
        // Apply the users PK specification
        applyUserPrimaryKeySpecification(pkmd);
      } else {
        // Define PK using internal rules
        if (orderRequired) {
          // Order column specified so owner+order are the PK
          orderMapping.getDatastoreMapping(0).getColumn().setPrimaryKey();
        } else {
          // No order column specified so owner+key are the PK
          for (int i = 0; i < keyMapping.getNumberOfDatastoreMappings(); i++) {
            keyMapping.getDatastoreMapping(i).getColumn().setPrimaryKey();
          }
        }
      }
    }

    if (NucleusLogger.DATASTORE_SCHEMA.isDebugEnabled()) {
      NucleusLogger.DATASTORE_SCHEMA.debug(Localiser.msg("057023", this));
    }
    storeMgr.registerTableInitialized(this);
    state = TABLE_STATE_INITIALIZED;
  }
  /**
   * Utility to create the datastore identity column and mapping. This is used in 2 modes. The first
   * is where we have a (primary) class table and we aren't creating the OID mapping as a FK to
   * another class. The second is where we have a (secondary) class table and we are creating the
   * OID mapping as a FK to the primary class. In the second case the refTable will be specified.
   *
   * @param columnMetaData The column MetaData for the datastore id
   * @param refTable Table used as a reference (if any)
   * @param cmd The MetaData for the class
   */
  void addDatastoreId(
      ColumnMetaData columnMetaData, DatastoreClass refTable, AbstractClassMetaData cmd) {
    // Create the mapping, setting its table
    datastoreIDMapping = new DatastoreIdMapping();
    datastoreIDMapping.setTable(this);
    datastoreIDMapping.initialize(storeMgr, cmd.getFullClassName());

    // Create a ColumnMetaData in the container if none is defined
    ColumnMetaData colmd = null;
    if (columnMetaData == null) {
      colmd = new ColumnMetaData();
    } else {
      colmd = columnMetaData;
    }
    if (colmd.getName() == null) {
      // Provide default column naming if none is defined
      if (refTable != null) {
        colmd.setName(
            storeMgr
                .getIdentifierFactory()
                .newColumnIdentifier(
                    refTable.getIdentifier().getName(),
                    this.storeMgr
                        .getNucleusContext()
                        .getTypeManager()
                        .isDefaultEmbeddedType(DatastoreId.class),
                    FieldRole.ROLE_OWNER,
                    false)
                .getName());
      } else {
        colmd.setName(
            storeMgr
                .getIdentifierFactory()
                .newColumnIdentifier(
                    identifier.getName(),
                    this.storeMgr
                        .getNucleusContext()
                        .getTypeManager()
                        .isDefaultEmbeddedType(DatastoreId.class),
                    FieldRole.ROLE_NONE,
                    false)
                .getName());
      }
    }

    // Add the datastore identity column as the PK
    Column idColumn =
        addColumn(
            DatastoreId.class.getName(),
            storeMgr.getIdentifierFactory().newIdentifier(IdentifierType.COLUMN, colmd.getName()),
            datastoreIDMapping,
            colmd);
    idColumn.setPrimaryKey();

    // Set the identity column type based on the IdentityStrategy
    String strategyName = cmd.getIdentityMetaData().getValueStrategy().toString();
    if (cmd.getIdentityMetaData().getValueStrategy().equals(IdentityStrategy.CUSTOM)) {
      strategyName = cmd.getIdentityMetaData().getValueStrategy().getCustomName();
    }
    if (strategyName != null && IdentityStrategy.NATIVE.toString().equals(strategyName)) {
      strategyName = storeMgr.getStrategyForNative(cmd, -1);
    }

    // Check the value generator type being stored
    Class valueGeneratedType = Long.class;
    if (strategyName != null && IdentityStrategy.IDENTITY.toString().equals(strategyName)) {
      valueGeneratedType = dba.getAutoIncrementJavaTypeForType(valueGeneratedType);
      if (valueGeneratedType != Long.class) {
        NucleusLogger.DATASTORE_SCHEMA.debug(
            "Class "
                + cmd.getFullClassName()
                + " uses IDENTITY strategy and rather than using BIGINT "
                + " for the column type, using "
                + valueGeneratedType.getName()
                + " since the datastore requires that");
      }
    }
    try {
      // Create generator so we can find the generated type
      // a). Try as unique generator first
      AbstractGenerator generator =
          (AbstractGenerator)
              storeMgr
                  .getNucleusContext()
                  .getPluginManager()
                  .createExecutableExtension(
                      "org.datanucleus.store_valuegenerator",
                      new String[] {"name", "unique"},
                      new String[] {strategyName, "true"},
                      "class-name",
                      new Class[] {String.class, Properties.class},
                      new Object[] {null, null});
      if (generator == null) {
        // b). Try as datastore-specific generator
        generator =
            (AbstractGenerator)
                storeMgr
                    .getNucleusContext()
                    .getPluginManager()
                    .createExecutableExtension(
                        "org.datanucleus.store_valuegenerator",
                        new String[] {"name", "datastore"},
                        new String[] {strategyName, storeMgr.getStoreManagerKey()},
                        "class-name",
                        new Class[] {String.class, Properties.class},
                        new Object[] {null, null});
      }
      try {
        if (generator != null) {
          ParameterizedType parameterizedType =
              (ParameterizedType) generator.getClass().getGenericSuperclass();
          valueGeneratedType = (Class) parameterizedType.getActualTypeArguments()[0];
          if (valueGeneratedType == null) {
            // Use getStorageClass method if available
            valueGeneratedType =
                (Class) generator.getClass().getMethod("getStorageClass").invoke(null);
          }
        }
      } catch (Exception e) {
      }
    } catch (Exception e) {
      NucleusLogger.VALUEGENERATION.warn(
          "Error obtaining generator for strategy=" + strategyName, e);
    }

    storeMgr
        .getMappingManager()
        .createDatastoreMapping(datastoreIDMapping, idColumn, valueGeneratedType.getName());
    logMapping("DATASTORE_ID", datastoreIDMapping);

    // Handle any auto-increment requirement
    if (isObjectIdDatastoreAttributed()) {
      if (this instanceof DatastoreClass && ((DatastoreClass) this).isBaseDatastoreClass()) {
        // Only the base class can be autoincremented
        idColumn.setIdentity(true);
      }
    }

    // Check if auto-increment and that it is supported by this RDBMS
    if (idColumn.isIdentity() && !dba.supportsOption(DatastoreAdapter.IDENTITY_COLUMNS)) {
      throw new NucleusException(
              Localiser.msg("057020", cmd.getFullClassName(), "datastore-identity"))
          .setFatal();
    }
  }
  /**
   * Utility to create the application identity columns and mapping. Uses the id mapping of the
   * specified class table and copies the mappings and columns, whilst retaining the passed
   * preferences for column namings. This is used to copy the PK mappings of a superclass table so
   * we have the same PK.
   *
   * @param columnContainer The container of column MetaData with any namings
   * @param refTable The table that we use as reference
   * @param clr The ClassLoaderResolver
   * @param cmd The ClassMetaData
   */
  final void addApplicationIdUsingClassTableId(
      ColumnMetaDataContainer columnContainer,
      DatastoreClass refTable,
      ClassLoaderResolver clr,
      AbstractClassMetaData cmd) {
    ColumnMetaData[] userdefinedCols = null;
    int nextUserdefinedCol = 0;
    if (columnContainer != null) {
      userdefinedCols = columnContainer.getColumnMetaData();
    }

    pkMappings = new JavaTypeMapping[cmd.getPKMemberPositions().length];
    for (int i = 0; i < cmd.getPKMemberPositions().length; i++) {
      AbstractMemberMetaData mmd =
          cmd.getMetaDataForManagedMemberAtAbsolutePosition(cmd.getPKMemberPositions()[i]);
      JavaTypeMapping mapping = refTable.getMemberMapping(mmd);
      if (mapping == null) {
        // probably due to invalid metadata defined by the user
        throw new NucleusUserException(
            "Cannot find mapping for field "
                + mmd.getFullFieldName()
                + " in table "
                + refTable.toString()
                + " "
                + StringUtils.collectionToString(refTable.getColumns()));
      }

      JavaTypeMapping masterMapping =
          storeMgr.getMappingManager().getMapping(clr.classForName(mapping.getType()));
      masterMapping.setMemberMetaData(mmd); // Update field info in mapping
      masterMapping.setTable(this);
      pkMappings[i] = masterMapping;

      // Loop through each id column in the reference table and add the same here
      // applying the required names from the columnContainer
      for (int j = 0; j < mapping.getNumberOfDatastoreMappings(); j++) {
        JavaTypeMapping m = masterMapping;
        Column refColumn = mapping.getDatastoreMapping(j).getColumn();
        if (mapping instanceof PersistableMapping) {
          m =
              storeMgr
                  .getMappingManager()
                  .getMapping(clr.classForName(refColumn.getJavaTypeMapping().getType()));
          ((PersistableMapping) masterMapping).addJavaTypeMapping(m);
        }

        ColumnMetaData userdefinedColumn = null;
        if (userdefinedCols != null) {
          for (int k = 0; k < userdefinedCols.length; k++) {
            if (refColumn.getIdentifier().toString().equals(userdefinedCols[k].getTarget())) {
              userdefinedColumn = userdefinedCols[k];
              break;
            }
          }
          if (userdefinedColumn == null && nextUserdefinedCol < userdefinedCols.length) {
            userdefinedColumn = userdefinedCols[nextUserdefinedCol++];
          }
        }

        // Add this application identity column
        Column idColumn = null;
        if (userdefinedColumn != null) {
          // User has provided a name for this column
          // Currently we only use the column namings from the users definition but we could easily
          // take more of their details.
          idColumn =
              addColumn(
                  refColumn.getStoredJavaType(),
                  storeMgr
                      .getIdentifierFactory()
                      .newIdentifier(IdentifierType.COLUMN, userdefinedColumn.getName()),
                  m,
                  refColumn.getColumnMetaData());
        } else {
          // No name provided so take same as superclass
          idColumn =
              addColumn(
                  refColumn.getStoredJavaType(),
                  refColumn.getIdentifier(),
                  m,
                  refColumn.getColumnMetaData());
        }
        if (mapping.getDatastoreMapping(j).getColumn().getColumnMetaData() != null) {
          refColumn.copyConfigurationTo(idColumn);
        }
        idColumn.setPrimaryKey();

        // Set the column type based on the field.getType()
        getStoreManager()
            .getMappingManager()
            .createDatastoreMapping(m, idColumn, refColumn.getJavaTypeMapping().getType());
      }

      // Update highest field number if this is higher
      int absoluteFieldNumber = mmd.getAbsoluteFieldNumber();
      if (absoluteFieldNumber > highestMemberNumber) {
        highestMemberNumber = absoluteFieldNumber;
      }
    }
  }