Java tutorial
/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 2011-2015 ForgeRock AS. All Rights Reserved * * The contents of this file are subject to the terms * of the Common Development and Distribution License * (the License). You may not use this file except in * compliance with the License. * * You can obtain a copy of the License at * http://forgerock.org/license/CDDLv1.0.html * See the License for the specific language governing * permission and limitations under the License. * * When distributing Covered Code, include this CDDL * Header Notice in each file and include the License file * at http://forgerock.org/license/CDDLv1.0.html * If applicable, add the following below the CDDL Header, * with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" */ package org.forgerock.openidm.repo.jdbc.impl; import static org.forgerock.json.resource.Responses.newResourceResponse; import static org.forgerock.openidm.repo.QueryConstants.PAGED_RESULTS_OFFSET; import static org.forgerock.openidm.repo.QueryConstants.PAGE_SIZE; import static org.forgerock.openidm.repo.QueryConstants.SORT_KEYS; import java.io.IOException; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.StringUtils; import org.forgerock.json.JsonPointer; import org.forgerock.json.JsonValue; import org.forgerock.json.resource.InternalServerErrorException; import org.forgerock.json.resource.NotFoundException; import org.forgerock.json.resource.PreconditionFailedException; import org.forgerock.json.resource.ResourceResponse; import org.forgerock.json.resource.ResourceException; import org.forgerock.json.resource.SortKey; import org.forgerock.openidm.config.enhanced.InvalidException; import org.forgerock.openidm.crypto.CryptoService; import org.forgerock.openidm.repo.jdbc.ErrorType; import org.forgerock.openidm.repo.jdbc.SQLExceptionHandler; import org.forgerock.openidm.repo.jdbc.TableHandler; import org.forgerock.openidm.repo.jdbc.impl.query.QueryResultMapper; import org.forgerock.openidm.repo.jdbc.impl.query.TableQueries; import org.forgerock.openidm.repo.util.StringSQLQueryFilterVisitor; import org.forgerock.openidm.repo.util.StringSQLRenderer; import org.forgerock.openidm.util.Accessor; import org.forgerock.openidm.util.JsonUtil; import org.forgerock.util.query.QueryFilter; import org.forgerock.util.query.QueryFilterVisitor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Handling of tables in a generic (not object specific) layout */ public class MappedTableHandler implements TableHandler { final static Logger logger = LoggerFactory.getLogger(MappedTableHandler.class); SQLExceptionHandler sqlExceptionHandler; final String tableName; String dbSchemaName; final LinkedHashMap<String, Object> rawMappingConfig; final Mapping explicitMapping; private final QueryFilterVisitor<StringSQLRenderer, Map<String, Object>, JsonPointer> queryFilterVisitor; // The json pointer (used as names) of the properties to replace the ? // tokens in the prepared statement, // in the order they need populating in create and update queries List<JsonPointer> tokenReplacementPropPointers = new ArrayList<JsonPointer>(); ObjectMapper mapper = new ObjectMapper(); final TableQueries queries; String readQueryStr; String readForUpdateQueryStr; String createQueryStr; String updateQueryStr; String deleteQueryStr; public MappedTableHandler(String tableName, Map<String, Object> mapping, String dbSchemaName, JsonValue queriesConfig, JsonValue commandsConfig, SQLExceptionHandler sqlExceptionHandler, Accessor<CryptoService> cryptoServiceAccessor) throws InternalServerErrorException { // TODO Replace this with a "guarantee" somewhere when/if the provision of this accessor becomes more automatic if (cryptoServiceAccessor == null) throw new InternalServerErrorException("No CryptoServiceAccessor found!"); this.tableName = tableName; this.dbSchemaName = dbSchemaName; // Maintain a stable ordering this.rawMappingConfig = new LinkedHashMap<String, Object>(); this.rawMappingConfig.putAll(mapping); explicitMapping = new Mapping(tableName, new JsonValue(rawMappingConfig), cryptoServiceAccessor); logger.debug("Explicit mapping: {}", explicitMapping); if (sqlExceptionHandler == null) { this.sqlExceptionHandler = new DefaultSQLExceptionHandler(); } else { this.sqlExceptionHandler = sqlExceptionHandler; } queryFilterVisitor = new StringSQLQueryFilterVisitor<Map<String, Object>>() { // value number for each value placeholder int objectNumber = 0; @Override public StringSQLRenderer visitValueAssertion(Map<String, Object> objects, String operand, JsonPointer field, Object valueAssertion) { ++objectNumber; String value = "v" + objectNumber; objects.put(value, valueAssertion); return new StringSQLRenderer( explicitMapping.getDbColumnName(field) + " " + operand + " ${" + value + "}"); } @Override public StringSQLRenderer visitPresentFilter(Map<String, Object> objects, JsonPointer field) { return new StringSQLRenderer(explicitMapping.getDbColumnName(field) + " IS NOT NULL"); } }; queries = new TableQueries(this, tableName, null, dbSchemaName, 0, new ExplicitQueryResultMapper(explicitMapping)); queries.setConfiguredQueries(tableName, dbSchemaName, queriesConfig, commandsConfig, null); initializeQueries(); } protected void initializeQueries() { final String mainTable = dbSchemaName == null ? tableName : dbSchemaName + "." + tableName; final StringBuffer colNames = new StringBuffer(); final StringBuffer tokenNames = new StringBuffer(); final StringBuffer prepTokens = new StringBuffer(); final StringBuffer updateAssign = new StringBuffer(); boolean isFirst = true; for (ColumnMapping colMapping : explicitMapping.columnMappings) { if (!isFirst) { colNames.append(", "); tokenNames.append(","); prepTokens.append(","); updateAssign.append(", "); } colNames.append(colMapping.dbColName); tokenNames.append("${").append(colMapping.objectColName).append("}"); prepTokens.append("?"); tokenReplacementPropPointers.add(colMapping.objectColPointer); // updateAssign.append(colMapping.dbColName).append(" = ${").append(colMapping.objectColName).append("}"); updateAssign.append(colMapping.dbColName).append(" = ?"); isFirst = false; } readQueryStr = "SELECT * FROM " + mainTable + " WHERE objectid = ?"; readForUpdateQueryStr = "SELECT * FROM " + mainTable + " WHERE objectid = ? FOR UPDATE"; createQueryStr = "INSERT INTO " + mainTable + " (" + colNames + ") VALUES ( " + prepTokens + ")"; updateQueryStr = "UPDATE " + mainTable + " SET " + updateAssign + " WHERE objectid = ?"; deleteQueryStr = "DELETE FROM " + mainTable + " WHERE objectid = ? AND rev = ?"; logger.debug("Unprepared query strings {} {} {} {} {}", readQueryStr, createQueryStr, updateQueryStr, deleteQueryStr); } /** * @see org.forgerock.openidm.repo.jdbc.TableHandler#read(java.lang.String, * java.lang.String, java.lang.String, java.sql.Connection) */ @Override public ResourceResponse read(String fullId, String type, String localId, Connection connection) throws NotFoundException, SQLException, IOException, InternalServerErrorException { JsonValue resultValue = null; ResourceResponse result = null; PreparedStatement readStatement = null; ResultSet rs = null; try { readStatement = queries.getPreparedStatement(connection, readQueryStr); logger.debug("Populating prepared statement {} for {}", readStatement, fullId); readStatement.setString(1, localId); logger.debug("Executing: {}", readStatement); rs = readStatement.executeQuery(); if (rs.next()) { resultValue = explicitMapping.mapToJsonValue(rs, Mapping.getColumnNames(rs)); JsonValue rev = resultValue.get("_rev"); logger.debug(" full id: {}, rev: {}, obj {}", fullId, rev, resultValue); result = newResourceResponse(localId, rev.asString(), resultValue); } else { throw new NotFoundException("Object " + fullId + " not found in " + type); } } finally { CleanupHelper.loggedClose(rs); CleanupHelper.loggedClose(readStatement); } return result; } /** * Reads an object with for update locking applied * * Note: statement associated with the returned resultset is not closed upon * return. Aside from taking care to close the resultset it also is the * responsibility of the caller to close the associated statement. Although * the specification specifies that drivers/pools should close the statement * automatically, not all do this reliably. * * @param fullId * qualified id of component type and id * @param type * the component type * @param localId * the id of the object within the component type * @param connection * the connection to use * @return the row for the requested object, selected FOR UPDATE * @throws NotFoundException * if the requested object was not found in the DB * @throws java.sql.SQLException * for general DB issues */ ResultSet readForUpdate(String fullId, String type, String localId, Connection connection) throws NotFoundException, SQLException { PreparedStatement readForUpdateStatement = null; ResultSet rs = null; try { readForUpdateStatement = queries.getPreparedStatement(connection, readForUpdateQueryStr); logger.trace("Populating prepared statement {} for {}", readForUpdateStatement, fullId); readForUpdateStatement.setString(1, localId); logger.debug("Executing: {}", readForUpdateStatement); rs = readForUpdateStatement.executeQuery(); if (rs.next()) { logger.debug("Read for update full id: {}", fullId); return rs; } else { CleanupHelper.loggedClose(rs); CleanupHelper.loggedClose(readForUpdateStatement); throw new NotFoundException("Object " + fullId + " not found in " + type); } } catch (SQLException ex) { CleanupHelper.loggedClose(rs); CleanupHelper.loggedClose(readForUpdateStatement); throw ex; } } /** * @see org.forgerock.openidm.repo.jdbc.TableHandler#create(java.lang.String, * java.lang.String, java.lang.String, java.util.Map, * java.sql.Connection) */ @Override public void create(String fullId, String type, String localId, Map<String, Object> obj, Connection connection) throws SQLException, IOException { PreparedStatement createStatement = queries.getPreparedStatement(connection, createQueryStr); try { create(fullId, type, localId, obj, connection, createStatement, false); } finally { CleanupHelper.loggedClose(createStatement); } } /** * Adds the option to batch more than one create statement * * @param batchCreate * if true just adds create to batched statements, does not * execute. false the statement is executed directly * @see org.forgerock.openidm.repo.jdbc.TableHandler#create(java.lang.String, * java.lang.String, java.lang.String, java.util.Map, * java.sql.Connection) for the other parameters */ protected void create(String fullId, String type, String localId, Map<String, Object> obj, Connection connection, PreparedStatement createStatement, boolean batchCreate) throws SQLException, IOException { logger.debug("Create with fullid {}", fullId); String rev = "0"; obj.put("_id", localId); // Save the id in the object obj.put("_rev", rev); // Save the rev in the object, and return the // changed rev from the create. JsonValue objVal = new JsonValue(obj); logger.debug("Preparing statement {} with {}, {}, {}", createStatement, type, localId, rev); populatePrepStatementColumns(createStatement, objVal, tokenReplacementPropPointers); if (!batchCreate) { logger.debug("Executing: {}", createStatement); int val = createStatement.executeUpdate(); logger.debug("Created object for id {} with rev {}", fullId, rev); } else { createStatement.addBatch(); logger.debug("Added create for object id {} with rev {} to batch", fullId, rev); } } /** * Populates the create or update statement with the token replacement * values in the appropriate order. For update the final objectid is not * part of the declarative mapping and needs to be populated separately. * * @param prepStatement * the update or create prepared statement * @param tokenPointers * the token replacement pointers pointing into the object set to * extract the relevant values * @return the buildNext column position if further populating is desired */ int populatePrepStatementColumns(PreparedStatement prepStatement, JsonValue objVal, List<JsonPointer> tokenPointers) throws IOException, SQLException { int colPos = 1; for (JsonPointer propPointer : tokenPointers) { // TODO: support explicit column types/conversion specified in // column mapping // This is currently limited to STRING handling JsonValue rawValue = objVal.get(propPointer); String propValue = null; if (null == rawValue) { propValue = null; } else if (rawValue.isString() || rawValue.isNull()) { propValue = rawValue.asString(); } else { if (logger.isTraceEnabled()) { logger.trace( "Value for col {} from {} is getting Stringified from type {} to store in a STRING column as value: {}", colPos, propPointer, rawValue.getClass(), rawValue); } propValue = mapper.writeValueAsString(rawValue.getObject()); } prepStatement.setString(colPos, propValue); colPos++; } return colPos; } /** * @see org.forgerock.openidm.repo.jdbc.TableHandler#update(java.lang.String, * java.lang.String, java.lang.String, java.lang.String, java.util.Map, * java.sql.Connection) */ @Override public void update(String fullId, String type, String localId, String rev, Map<String, Object> obj, Connection connection) throws SQLException, IOException, PreconditionFailedException, NotFoundException, InternalServerErrorException { logger.debug("Update with fullid {}", fullId); int revInt = Integer.parseInt(rev); ++revInt; String newRev = Integer.toString(revInt); obj.put("_rev", newRev); // Save the rev in the object, and return the // changed rev from the create. ResultSet rs = null; PreparedStatement updateStatement = null; try { rs = readForUpdate(fullId, type, localId, connection); String existingRev = explicitMapping.getRev(rs); logger.debug("Update existing object {} rev: {} ", fullId, existingRev); if (!existingRev.equals(rev)) { throw new PreconditionFailedException("Update rejected as current Object revision " + existingRev + " is different than expected by caller (" + rev + "), the object has changed since retrieval."); } updateStatement = queries.getPreparedStatement(connection, updateQueryStr); // Support changing object identifier String newLocalId = (String) obj.get("_id"); if (newLocalId != null && !localId.equals(newLocalId)) { logger.debug("Object identifier is changing from " + localId + " to " + newLocalId); } else { newLocalId = localId; // If it hasn't changed, use the existing // ID obj.put("_id", newLocalId); // Ensure the ID is saved in the // object } JsonValue objVal = new JsonValue(obj); logger.trace("Populating prepared statement {} for {} {} {}", updateStatement, fullId, newLocalId, newRev); int nextCol = populatePrepStatementColumns(updateStatement, objVal, tokenReplacementPropPointers); updateStatement.setString(nextCol, localId); logger.debug("Update statement: {}", updateStatement); int updateCount = updateStatement.executeUpdate(); logger.trace("Updated rows: {} for {}", updateCount, fullId); if (updateCount != 1) { throw new InternalServerErrorException( "Update execution did not result in updating 1 row as expected. Updated rows: " + updateCount); } } finally { if (rs != null) { // Ensure associated statement also is closed Statement rsStatement = rs.getStatement(); CleanupHelper.loggedClose(rs); CleanupHelper.loggedClose(rsStatement); } CleanupHelper.loggedClose(updateStatement); } } /** * @see org.forgerock.openidm.repo.jdbc.TableHandler#delete(java.lang.String, * java.lang.String, java.lang.String, java.lang.String, * java.sql.Connection) */ @Override public void delete(String fullId, String type, String localId, String rev, Connection connection) throws PreconditionFailedException, InternalServerErrorException, NotFoundException, SQLException, IOException { logger.debug("Delete with fullid {}", fullId); // First check if the revision matches and select it for UPDATE ResultSet existing = null; PreparedStatement deleteStatement = null; try { try { existing = readForUpdate(fullId, type, localId, connection); } catch (NotFoundException ex) { throw new NotFoundException("Object does not exist for delete on: " + fullId); } String existingRev = explicitMapping.getRev(existing); if (!"*".equals(rev) && !rev.equals(existingRev)) { throw new PreconditionFailedException( "Delete rejected as current Object revision " + existingRev + " is different than " + "expected by caller " + rev + ", the object has changed since retrieval."); } // Proceed with the valid delete deleteStatement = queries.getPreparedStatement(connection, deleteQueryStr); logger.trace("Populating prepared statement {} for {} {} {} {}", deleteStatement, fullId, type, localId, rev); deleteStatement.setString(1, localId); deleteStatement.setString(2, rev); logger.debug("Delete statement: {}", deleteStatement); int deletedRows = deleteStatement.executeUpdate(); logger.trace("Deleted {} rows for id : {} {}", deletedRows, localId); if (deletedRows < 1) { throw new InternalServerErrorException( "Deleting object for " + fullId + " failed, DB reported " + deletedRows + " rows deleted"); } else { logger.debug("delete for id succeeded: {} revision: {}", localId, rev); } } finally { if (existing != null) { // Ensure associated statement also is closed Statement existingStatement = existing.getStatement(); CleanupHelper.loggedClose(existing); CleanupHelper.loggedClose(existingStatement); } CleanupHelper.loggedClose(deleteStatement); } } /** * @see org.forgerock.openidm.repo.jdbc.TableHandler#delete(java.lang.String, * java.lang.String, java.lang.String, java.lang.String, * java.sql.Connection) */ @Override public List<Map<String, Object>> query(String type, Map<String, Object> params, Connection connection) throws ResourceException { return queries.query(type, params, connection); } @Override public Integer command(String type, Map<String, Object> params, Connection connection) throws SQLException, ResourceException { return queries.command(type, params, connection); } @Override public boolean queryIdExists(String queryId) { return queries.queryIdExists(queryId); } // TODO: make common to generic and explicit handlers /** * @inheritDoc */ public boolean isErrorType(SQLException ex, ErrorType errorType) { return sqlExceptionHandler.isErrorType(ex, errorType); } // TODO: make common to generic and explicit handlers /** * InheritDoc */ public boolean isRetryable(SQLException ex, Connection connection) { return sqlExceptionHandler.isRetryable(ex, connection); } @Override public String toString() { return "Generic handler mapped to " + tableName + " and mapping " + rawMappingConfig; } @Override public String renderQueryFilter(QueryFilter<JsonPointer> filter, Map<String, Object> replacementTokens, Map<String, Object> params) { final String offsetParam = (String) params.get(PAGED_RESULTS_OFFSET); final String pageSizeParam = (String) params.get(PAGE_SIZE); String pageClause = " LIMIT " + pageSizeParam + " OFFSET " + offsetParam; // JsonValue-cheat to avoid an unchecked cast final List<SortKey> sortKeys = new JsonValue(params).get(SORT_KEYS).asList(SortKey.class); // Check for sort keys and build up order-by syntax if (sortKeys != null && sortKeys.size() > 0) { List<String> keys = new ArrayList<String>(); for (int i = 0; i < sortKeys.size(); i++) { SortKey sortKey = sortKeys.get(i); String tokenName = "sortKey" + i; keys.add("${" + tokenName + "}" + (sortKey.isAscendingOrder() ? " ASC" : " DESC")); replacementTokens.put(tokenName, sortKey.getField().toString().substring(1)); } pageClause = " ORDER BY " + StringUtils.join(keys, ", ") + pageClause; } return "SELECT obj.* FROM ${_dbSchema}.${_mainTable} obj" + getFilterString(filter, replacementTokens) + pageClause; } /** * Loops through sort keys constructing the key statements. * * @param sortKeys a {@link List} of sort keys * @param keys a {@link List} to store ORDER BY keys * @param replacementTokens a {@link Map} containing replacement tokens for the {@link PreparedStatement} */ protected void prepareSortKeyStatements(List<SortKey> sortKeys, List<String> keys, Map<String, Object> replacementTokens) { for (int i = 0; i < sortKeys.size(); i++) { SortKey sortKey = sortKeys.get(i); keys.add(explicitMapping.getDbColumnName(sortKey.getField()) + (sortKey.isAscendingOrder() ? " ASC" : " DESC")); } } /** * Returns a query string representing the supplied filter. * * @param filter the {@link QueryFilter} object * @param replacementTokens replacement tokens for the query string * @return a query string */ protected String getFilterString(QueryFilter<JsonPointer> filter, Map<String, Object> replacementTokens) { return " WHERE " + filter.accept(queryFilterVisitor, replacementTokens).toSQL(); } } /** * Handle the conversion of query results to the object set model */ class ExplicitQueryResultMapper implements QueryResultMapper { final static Logger logger = LoggerFactory.getLogger(ExplicitQueryResultMapper.class); Mapping explicitMapping; public ExplicitQueryResultMapper(Mapping explicitMapping) { this.explicitMapping = explicitMapping; } public List<Map<String, Object>> mapQueryToObject(ResultSet rs, String queryId, String type, Map<String, Object> params, TableQueries tableQueries) throws SQLException, InternalServerErrorException { List<Map<String, Object>> result = new ArrayList<Map<String, Object>>(); Set<String> names = Mapping.getColumnNames(rs); while (rs.next()) { JsonValue obj = explicitMapping.mapToJsonValue(rs, names); result.add(obj.asMap()); } return result; } } /** * Parsed Config handling */ class Mapping { final static Logger logger = LoggerFactory.getLogger(Mapping.class); String tableName; Accessor<CryptoService> cryptoServiceAccessor; List<ColumnMapping> columnMappings = new ArrayList<ColumnMapping>(); ColumnMapping revMapping; // Quick access to mapping for MVCC revision ObjectMapper mapper = new ObjectMapper(); public Mapping(String tableName, JsonValue mappingConfig, Accessor<CryptoService> cryptoServiceAccessor) { this.cryptoServiceAccessor = cryptoServiceAccessor; this.tableName = tableName; for (Map.Entry<String, Object> entry : mappingConfig.asMap().entrySet()) { String key = entry.getKey(); JsonValue value = mappingConfig.get(key); ColumnMapping colMapping = new ColumnMapping(key, value); columnMappings.add(colMapping); if ("_rev".equals(colMapping.objectColName)) { revMapping = colMapping; } } } public JsonValue mapToJsonValue(ResultSet rs, Set<String> columnNames) throws SQLException, InternalServerErrorException { JsonValue mappedResult = new JsonValue(new LinkedHashMap<String, Object>()); for (ColumnMapping entry : columnMappings) { Object value = null; if (columnNames.contains(entry.dbColName)) { if (ColumnMapping.TYPE_STRING.equals(entry.dbColType)) { value = rs.getString(entry.dbColName); if (cryptoServiceAccessor == null || cryptoServiceAccessor.access() == null) { throw new InternalServerErrorException("CryptoService unavailable"); } if (JsonUtil.isEncrypted((String) value)) { value = convertToJson(entry.dbColName, "encrypted", (String) value, Map.class).asMap(); } } else if (ColumnMapping.TYPE_JSON_MAP.equals(entry.dbColType)) { value = convertToJson(entry.dbColName, entry.dbColType, rs.getString(entry.dbColName), Map.class).asMap(); } else if (ColumnMapping.TYPE_JSON_LIST.equals(entry.dbColType)) { value = convertToJson(entry.dbColName, entry.dbColType, rs.getString(entry.dbColName), List.class).asList(); } else { throw new InternalServerErrorException("Unsupported DB column type " + entry.dbColType); } mappedResult.putPermissive(entry.objectColPointer, value); } } logger.debug("Mapped rs {} to {}", rs, mappedResult); return mappedResult; } private <T> JsonValue convertToJson(String name, String nameType, String value, Class<T> valueType) throws InternalServerErrorException { if (value != null) { try { return new JsonValue(mapper.readValue(value, valueType)); } catch (IOException e) { throw new InternalServerErrorException("Unable to map " + nameType + " value for " + name, e); } } return new JsonValue(null); } public String getRev(ResultSet rs) throws SQLException { return rs.getString(revMapping.dbColName); } public String toString() { StringBuffer sb = new StringBuffer(); sb.append("Explicit table mapping for " + tableName + " :\n"); for (ColumnMapping entry : columnMappings) { sb.append(entry.toString()); } return sb.toString(); } public static Set<String> getColumnNames(ResultSet rs) throws SQLException { TreeSet<String> set = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER); for (int i = 1; i <= rs.getMetaData().getColumnCount(); i++) { set.add(rs.getMetaData().getColumnName(i)); } return set; } public String getDbColumnName(JsonPointer fieldName) { for (ColumnMapping column : columnMappings) { if (column.isJsonPointer(fieldName)) { return column.dbColName; } } throw new IllegalArgumentException("Unknown object field: " + fieldName.toString()); } } /** * Parsed Config handling */ class ColumnMapping { public static final String DB_COLUMN_NAME = "column"; public static final String DB_COLUMN_TYPE = "type"; public static final String TYPE_STRING = "STRING"; public static final String TYPE_JSON_MAP = "JSON_MAP"; public static final String TYPE_JSON_LIST = "JSON_LIST"; public JsonPointer objectColPointer; public String objectColName; // String representation of the column // name/path public String dbColName; public String dbColType; public ColumnMapping(String objectColName, JsonValue dbColMappingConfig) { this.objectColName = objectColName; this.objectColPointer = new JsonPointer(objectColName); if (dbColMappingConfig.required().isList()) { if (dbColMappingConfig.asList().size() != 2) { throw new InvalidException("Explicit table mapping has invalid entry for " + objectColName + ", expecting column name and type but contains " + dbColMappingConfig.asList()); } dbColName = dbColMappingConfig.get(0).required().asString(); dbColType = dbColMappingConfig.get(1).required().asString(); } else if (dbColMappingConfig.isMap()) { dbColName = dbColMappingConfig.asMap().get(DB_COLUMN_NAME).toString(); dbColType = dbColMappingConfig.asMap().get(DB_COLUMN_TYPE).toString(); } else { dbColName = dbColMappingConfig.asString(); dbColType = TYPE_STRING; } } public boolean isJsonPointer(JsonPointer fieldPointer) { return objectColPointer.equals(fieldPointer); } public String toString() { return "object column : " + objectColName + " -> " + dbColName + ":" + dbColType + "\n"; } }