/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.cayenne.dba; import org.apache.cayenne.CayenneRuntimeException; import org.apache.cayenne.access.DataNode; import org.apache.cayenne.access.translator.ParameterBinding; import org.apache.cayenne.access.translator.batch.BatchTranslatorFactory; import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory; import org.apache.cayenne.access.translator.ejbql.JdbcEJBQLTranslatorFactory; import org.apache.cayenne.access.translator.select.DefaultSelectTranslator; import org.apache.cayenne.access.translator.select.QualifierTranslator; import org.apache.cayenne.access.translator.select.QueryAssembler; import org.apache.cayenne.access.translator.select.SelectTranslator; import org.apache.cayenne.access.types.ExtendedType; import org.apache.cayenne.access.types.ExtendedTypeFactory; import org.apache.cayenne.access.types.ExtendedTypeMap; import org.apache.cayenne.access.types.ValueObjectTypeFactory; import org.apache.cayenne.access.types.ValueObjectTypeRegistry; import org.apache.cayenne.configuration.Constants; import org.apache.cayenne.configuration.RuntimeProperties; import org.apache.cayenne.di.Inject; import org.apache.cayenne.log.JdbcEventLogger; import org.apache.cayenne.map.DbAttribute; import org.apache.cayenne.map.DbEntity; import org.apache.cayenne.map.DbJoin; import org.apache.cayenne.map.DbRelationship; import org.apache.cayenne.map.EntityResolver; import org.apache.cayenne.query.Query; import org.apache.cayenne.query.SQLAction; import org.apache.cayenne.query.SelectQuery; import org.apache.cayenne.resource.Resource; import org.apache.cayenne.resource.ResourceLocator; import org.apache.cayenne.util.Util; import java.net.URL; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Types; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; /** * A generic DbAdapter implementation. Can be used as a default adapter or as a * superclass of a concrete adapter implementation. */ public class JdbcAdapter implements DbAdapter { private PkGenerator pkGenerator; protected QuotingStrategy quotingStrategy; protected TypesHandler typesHandler; protected ExtendedTypeMap extendedTypes; protected boolean supportsBatchUpdates; protected boolean supportsUniqueConstraints; protected boolean supportsGeneratedKeys; protected EJBQLTranslatorFactory ejbqlTranslatorFactory; protected ResourceLocator resourceLocator; protected boolean caseInsensitiveCollations; /** * @since 3.1 * @deprecated since 4.0 BatchQueryBuilderfactory is attached to the DataNode. */ @Inject protected BatchTranslatorFactory batchQueryBuilderFactory; @Inject protected JdbcEventLogger logger; /** * Creates new JdbcAdapter with a set of default parameters. */ public JdbcAdapter(@Inject RuntimeProperties runtimeProperties, @Inject(Constants.SERVER_DEFAULT_TYPES_LIST) List<ExtendedType> defaultExtendedTypes, @Inject(Constants.SERVER_USER_TYPES_LIST) List<ExtendedType> userExtendedTypes, @Inject(Constants.SERVER_TYPE_FACTORIES_LIST) List<ExtendedTypeFactory> extendedTypeFactories, @Inject(Constants.SERVER_RESOURCE_LOCATOR) ResourceLocator resourceLocator, @Inject ValueObjectTypeRegistry valueObjectTypeRegistry) { // init defaults this.setSupportsBatchUpdates(false); this.setSupportsUniqueConstraints(true); this.caseInsensitiveCollations = runtimeProperties.getBoolean(Constants.CI_PROPERTY, false); this.resourceLocator = resourceLocator; this.pkGenerator = createPkGenerator(); this.quotingStrategy = createQuotingStrategy(); this.ejbqlTranslatorFactory = createEJBQLTranslatorFactory(); this.typesHandler = TypesHandler.getHandler(findResource("/types.xml")); this.extendedTypes = new ExtendedTypeMap(); initExtendedTypes(defaultExtendedTypes, userExtendedTypes, extendedTypeFactories, valueObjectTypeRegistry); } /** * Returns default separator - a semicolon. * * @since 1.0.4 */ @Override public String getBatchTerminator() { return ";"; } /** * @since 3.1 */ public JdbcEventLogger getJdbcEventLogger() { return this.logger; } /** * Locates and returns a named adapter resource. A resource can be an XML * file, etc. * <p> * This implementation is based on the premise that each adapter is located * in its own Java package and all resources are in the same package as * well. Resource lookup is recursive, so that if DbAdapter is a subclass of * another adapter, parent adapter package is searched as a failover. * </p> * * @since 3.0 */ protected URL findResource(String name) { Class<?> adapterClass = getClass(); while (adapterClass != null && JdbcAdapter.class.isAssignableFrom(adapterClass)) { String path = Util.getPackagePath(adapterClass.getName()) + name; Collection<Resource> resources = resourceLocator.findResources(path); if (!resources.isEmpty()) { return resources.iterator().next().getURL(); } adapterClass = adapterClass.getSuperclass(); } return null; } /** * Called from {@link #initExtendedTypes(List, List, List, ValueObjectTypeRegistry)} to load * adapter-specific types into the ExtendedTypeMap right after the default * types are loaded, but before the DI overrides are. This method has * specific implementations in JdbcAdapter subclasses. */ protected void configureExtendedTypes(ExtendedTypeMap map) { // noop... subclasses may override to install custom types } /** * @since 3.1 */ protected void initExtendedTypes(List<ExtendedType> defaultExtendedTypes, List<ExtendedType> userExtendedTypes, List<ExtendedTypeFactory> extendedTypeFactories, ValueObjectTypeRegistry valueObjectTypeRegistry) { for (ExtendedType type : defaultExtendedTypes) { extendedTypes.registerType(type); } // loading adapter specific extended types configureExtendedTypes(extendedTypes); for (ExtendedType type : userExtendedTypes) { extendedTypes.registerType(type); } for (ExtendedTypeFactory typeFactory : extendedTypeFactories) { extendedTypes.addFactory(typeFactory); } extendedTypes.addFactory(new ValueObjectTypeFactory(extendedTypes, valueObjectTypeRegistry)); } /** * Creates and returns a primary key generator. This factory method should * be overriden by JdbcAdapter subclasses to provide custom implementations * of PKGenerator. */ protected PkGenerator createPkGenerator() { return new JdbcPkGenerator(this); } /** * Creates and returns an {@link EJBQLTranslatorFactory} used to generate * visitors for EJBQL to SQL translations. This method should be overriden * by subclasses that need to customize EJBQL generation. * * @since 3.0 */ protected EJBQLTranslatorFactory createEJBQLTranslatorFactory() { JdbcEJBQLTranslatorFactory translatorFactory = new JdbcEJBQLTranslatorFactory(); translatorFactory.setCaseInsensitive(caseInsensitiveCollations); return translatorFactory; } /** * Returns primary key generator associated with this DbAdapter. */ @Override public PkGenerator getPkGenerator() { return pkGenerator; } /** * Sets new primary key generator. * * @since 1.1 */ public void setPkGenerator(PkGenerator pkGenerator) { this.pkGenerator = pkGenerator; } /** * Returns true. * * @since 1.1 */ @Override public boolean supportsUniqueConstraints() { return supportsUniqueConstraints; } /** * Returns true. * * @since 4.0 */ @Override public boolean supportsCatalogsOnReverseEngineering() { return true; } /** * @since 1.1 */ public void setSupportsUniqueConstraints(boolean flag) { this.supportsUniqueConstraints = flag; } /** * Returns true if supplied type can have a length attribute as a part of * column definition * * @since 4.0 */ public boolean typeSupportsLength(int type) { return JdbcAdapter.supportsLength(type); } /** * Returns true if supplied type can have a length attribute as a part of * column definition * <p/> * TODO: this is a static method only to support the deprecated method * {@link TypesMapping#supportsLength(int)} When the deprecated method is * removed this body should be moved in to {@link #typeSupportsLength(int)} * * @deprecated */ static boolean supportsLength(int type) { return type == Types.BINARY || type == Types.CHAR || type == Types.NCHAR || type == Types.NVARCHAR || type == Types.LONGNVARCHAR || type == Types.DECIMAL || type == Types.DOUBLE || type == Types.FLOAT || type == Types.NUMERIC || type == Types.REAL || type == Types.VARBINARY || type == Types.VARCHAR; } /** * @since 3.0 */ @Override public Collection<String> dropTableStatements(DbEntity table) { return Collections.singleton("DROP TABLE " + quotingStrategy.quotedFullyQualifiedName(table)); } /** * Returns a SQL string that can be used to create database table * corresponding to <code>ent</code> parameter. */ @Override public String createTable(DbEntity entity) { StringBuffer sqlBuffer = new StringBuffer(); sqlBuffer.append("CREATE TABLE "); sqlBuffer.append(quotingStrategy.quotedFullyQualifiedName(entity)); sqlBuffer.append(" ("); // columns Iterator<DbAttribute> it = entity.getAttributes().iterator(); if (it.hasNext()) { boolean first = true; while (it.hasNext()) { if (first) { first = false; } else { sqlBuffer.append(", "); } DbAttribute column = it.next(); // attribute may not be fully valid, do a simple check if (column.getType() == TypesMapping.NOT_DEFINED) { throw new CayenneRuntimeException("Undefined type for attribute '%s.%s'." , entity.getFullyQualifiedName(), column.getName()); } createTableAppendColumn(sqlBuffer, column); } createTableAppendPKClause(sqlBuffer, entity); } sqlBuffer.append(')'); return sqlBuffer.toString(); } /** * @since 1.2 */ protected void createTableAppendPKClause(StringBuffer sqlBuffer, DbEntity entity) { Iterator<DbAttribute> pkit = entity.getPrimaryKeys().iterator(); if (pkit.hasNext()) { sqlBuffer.append(", PRIMARY KEY ("); boolean firstPk = true; while (pkit.hasNext()) { if (firstPk) { firstPk = false; } else { sqlBuffer.append(", "); } DbAttribute at = pkit.next(); sqlBuffer.append(quotingStrategy.quotedName(at)); } sqlBuffer.append(')'); } } /** * Appends SQL for column creation to CREATE TABLE buffer. * * @since 1.2 */ @Override public void createTableAppendColumn(StringBuffer sqlBuffer, DbAttribute column) { sqlBuffer.append(quotingStrategy.quotedName(column)); sqlBuffer.append(' ').append(getType(this, column)); sqlBuffer.append(sizeAndPrecision(this, column)); sqlBuffer.append(column.isMandatory() ? " NOT NULL" : " NULL"); } public static String sizeAndPrecision(DbAdapter adapter, DbAttribute column) { if (!adapter.typeSupportsLength(column.getType())) { return ""; } int len = column.getMaxLength(); int scale = TypesMapping.isDecimal(column.getType()) && column.getType() != Types.FLOAT ? column.getScale() : -1; // sanity check if (scale > len) { scale = -1; } if (len > 0) { return "(" + len + (scale >= 0 ? ", " + scale : "") + ")"; } return ""; } public static String getType(DbAdapter adapter, DbAttribute column) { String[] types = adapter.externalTypesForJdbcType(column.getType()); if (types == null || types.length == 0) { String entityName = column.getEntity() != null ? column.getEntity().getFullyQualifiedName() : "<null>"; throw new CayenneRuntimeException("Undefined type for attribute '%s.%s': %s." , entityName, column.getName(), column.getType()); } return types[0]; } /** * Returns a DDL string to create a unique constraint over a set of columns. * * @since 1.1 */ @Override public String createUniqueConstraint(DbEntity source, Collection<DbAttribute> columns) { if (columns == null || columns.isEmpty()) { throw new CayenneRuntimeException("Can't create UNIQUE constraint - no columns specified."); } StringBuilder buf = new StringBuilder(); buf.append("ALTER TABLE "); buf.append(quotingStrategy.quotedFullyQualifiedName(source)); buf.append(" ADD UNIQUE ("); Iterator<DbAttribute> it = columns.iterator(); DbAttribute first = it.next(); buf.append(quotingStrategy.quotedName(first)); while (it.hasNext()) { DbAttribute next = it.next(); buf.append(", "); buf.append(quotingStrategy.quotedName(next)); } buf.append(")"); return buf.toString(); } /** * Returns a SQL string that can be used to create a foreign key constraint * for the relationship. */ @Override public String createFkConstraint(DbRelationship rel) { DbEntity source = rel.getSourceEntity(); StringBuilder buf = new StringBuilder(); StringBuilder refBuf = new StringBuilder(); buf.append("ALTER TABLE "); buf.append(quotingStrategy.quotedFullyQualifiedName(source)); buf.append(" ADD FOREIGN KEY ("); boolean first = true; for (DbJoin join : rel.getJoins()) { if (first) { first = false; } else { buf.append(", "); refBuf.append(", "); } buf.append(quotingStrategy.quotedSourceName(join)); refBuf.append(quotingStrategy.quotedTargetName(join)); } buf.append(") REFERENCES "); buf.append(quotingStrategy.quotedFullyQualifiedName(rel.getTargetEntity())); buf.append(" (").append(refBuf.toString()).append(')'); return buf.toString(); } @Override public String[] externalTypesForJdbcType(int type) { return typesHandler.externalTypesForJdbcType(type); } @Override public ExtendedTypeMap getExtendedTypes() { return extendedTypes; } @Override public DbAttribute buildAttribute(String name, String typeName, int type, int size, int scale, boolean allowNulls) { DbAttribute attr = new DbAttribute(); attr.setName(name); attr.setType(type); attr.setMandatory(!allowNulls); if (size >= 0) { attr.setMaxLength(size); } if (scale >= 0) { attr.setScale(scale); } return attr; } @Override public String tableTypeForTable() { return "TABLE"; } @Override public String tableTypeForView() { return "VIEW"; } /** * Creates and returns a default implementation of a qualifier translator. */ @Override public QualifierTranslator getQualifierTranslator(QueryAssembler queryAssembler) { QualifierTranslator translator = new QualifierTranslator(queryAssembler); translator.setCaseInsensitive(caseInsensitiveCollations); return translator; } /** * Uses JdbcActionBuilder to create the right action. * * @since 1.2 */ @Override public SQLAction getAction(Query query, DataNode node) { return query.createSQLAction(new JdbcActionBuilder(node)); } @Override public SelectTranslator getSelectTranslator(SelectQuery<?> query, EntityResolver entityResolver) { return new DefaultSelectTranslator(query, this, entityResolver); } @Override public void bindParameter(PreparedStatement statement, ParameterBinding binding) throws SQLException, Exception { if (binding.getValue() == null) { statement.setNull(binding.getStatementPosition(), binding.getJdbcType()); } else { binding.getExtendedType().setJdbcObject(statement, binding.getValue(), binding.getStatementPosition(), binding.getJdbcType(), binding.getScale()); } } @Override public boolean supportsBatchUpdates() { return this.supportsBatchUpdates; } public void setSupportsBatchUpdates(boolean flag) { this.supportsBatchUpdates = flag; } /** * @since 1.2 */ @Override public boolean supportsGeneratedKeys() { return supportsGeneratedKeys; } /** * @since 1.2 */ public void setSupportsGeneratedKeys(boolean flag) { this.supportsGeneratedKeys = flag; } /** * Returns a translator factory for EJBQL to SQL translation. The factory is * normally initialized in constructor by calling * {@link #createEJBQLTranslatorFactory()}, and can be changed later by * calling {@link #setEjbqlTranslatorFactory(EJBQLTranslatorFactory)}. * * @since 3.0 */ public EJBQLTranslatorFactory getEjbqlTranslatorFactory() { return ejbqlTranslatorFactory; } /** * Sets a translator factory for EJBQL to SQL translation. This property is * normally initialized in constructor by calling * {@link #createEJBQLTranslatorFactory()}, so users would only override it * if they need to customize EJBQL translation. * * @since 3.0 */ public void setEjbqlTranslatorFactory(EJBQLTranslatorFactory ejbqlTranslatorFactory) { this.ejbqlTranslatorFactory = ejbqlTranslatorFactory; } /** * @return * @since 4.0 */ protected QuotingStrategy createQuotingStrategy() { return new DefaultQuotingStrategy("\"", "\""); } /** * @since 3.0 * @deprecated since 4.0 use {@link #getQuotingStrategy()}. */ @Deprecated public QuotingStrategy getQuotingStrategy(boolean needQuotes) { return getQuotingStrategy(); } /** * @since 4.0 */ public QuotingStrategy getQuotingStrategy() { return quotingStrategy; } /** * @since 3.1 * @deprecated since 4.0 BatchQueryBuilderfactory is attached to the * DataNode. */ @Deprecated public BatchTranslatorFactory getBatchQueryBuilderFactory() { return batchQueryBuilderFactory; } /** * @since 3.1 * @deprecated since 4.0 BatchQueryBuilderfactory is attached to the * DataNode. */ @Deprecated public void setBatchQueryBuilderFactory(BatchTranslatorFactory batchQueryBuilderFactory) { this.batchQueryBuilderFactory = batchQueryBuilderFactory; } /** * Simply returns this, as JdbcAdapter is not a wrapper. * * @since 4.0 */ @Override public DbAdapter unwrap() { return this; } }