001package io.ebean.config;
002
003import io.ebean.config.dbplatform.DatabasePlatform;
004import io.ebean.util.AnnotationUtil;
005
006import javax.persistence.DiscriminatorValue;
007import javax.persistence.Inheritance;
008import javax.persistence.Table;
009
010import static io.ebean.util.StringHelper.isNull;
011
012/**
013 * Provides some base implementation for NamingConventions.
014 *
015 * @author emcgreal
016 */
017public abstract class AbstractNamingConvention implements NamingConvention {
018
019  /**
020   * The Constant DEFAULT_SEQ_FORMAT.
021   */
022  public static final String DEFAULT_SEQ_FORMAT = "{table}_seq";
023
024  /**
025   * The catalog.
026   */
027  private String catalog;
028
029  /**
030   * The schema.
031   */
032  private String schema;
033
034  /**
035   * The sequence format.
036   */
037  private String sequenceFormat;
038
039  /**
040   * The database platform.
041   */
042  protected DatabasePlatform databasePlatform;
043
044  /**
045   * Used to trim off extra prefix for M2M.
046   */
047  protected int rhsPrefixLength = 3;
048
049  protected boolean useForeignKeyPrefix;
050
051  /**
052   * Construct with a sequence format and useForeignKeyPrefix setting.
053   */
054  public AbstractNamingConvention(String sequenceFormat, boolean useForeignKeyPrefix) {
055    this.sequenceFormat = sequenceFormat;
056    this.useForeignKeyPrefix = useForeignKeyPrefix;
057  }
058
059  /**
060   * Construct with a sequence format.
061   *
062   * @param sequenceFormat the sequence format
063   */
064  public AbstractNamingConvention(String sequenceFormat) {
065    this.sequenceFormat = sequenceFormat;
066    this.useForeignKeyPrefix = true;
067  }
068
069  /**
070   * Construct with the default sequence format ("{table}_seq") and useForeignKeyPrefix as true.
071   */
072  public AbstractNamingConvention() {
073    this(DEFAULT_SEQ_FORMAT);
074  }
075
076  @Override
077  public void setDatabasePlatform(DatabasePlatform databasePlatform) {
078    this.databasePlatform = databasePlatform;
079  }
080
081  @Override
082  public String getSequenceName(String rawTableName, String pkColumn) {
083    TableName tableName = new TableName(rawTableName);
084    String seqName = seqName(pkColumn, tableName.getName());
085    return tableName.withCatalogAndSchema(seqName);
086  }
087
088  private String seqName(String pkColumn, String tableName) {
089    final String tableNameUnquoted = unQuote(tableName);
090    String seqName = sequenceFormat.replace("{table}", tableNameUnquoted);
091    pkColumn = (pkColumn == null) ? "" : unQuote(pkColumn);
092    return quoteIdentifiers(seqName.replace("{column}", pkColumn));
093  }
094
095  /**
096   * Return the catalog.
097   */
098  public String getCatalog() {
099    return catalog;
100  }
101
102  /**
103   * Sets the catalog.
104   */
105  public void setCatalog(String catalog) {
106    this.catalog = catalog;
107  }
108
109  /**
110   * Return the schema.
111   */
112  public String getSchema() {
113    return schema;
114  }
115
116  /**
117   * Sets the schema.
118   */
119  public void setSchema(String schema) {
120    this.schema = schema;
121  }
122
123  /**
124   * Returns the sequence format.
125   */
126  public String getSequenceFormat() {
127    return sequenceFormat;
128  }
129
130  /**
131   * Set the sequence format used to generate the sequence name.
132   * <p>
133   * The format should include "{table}". When generating the sequence name
134   * {table} is replaced with the actual table name.
135   * </p>
136   *
137   * @param sequenceFormat string containing "{table}" which is replaced with the actual
138   *                       table name to generate the sequence name.
139   */
140  public void setSequenceFormat(String sequenceFormat) {
141    this.sequenceFormat = sequenceFormat;
142  }
143
144  /**
145   * Return true if a prefix should be used building a foreign key name.
146   * <p>
147   * This by default is true and this works well when the primary key column
148   * names are simply "ID". In this case a prefix (such as "ORDER" and
149   * "CUSTOMER" etc) is added to the foreign key column producing "ORDER_ID" and
150   * "CUSTOMER_ID".
151   * </p>
152   * <p>
153   * This should return false when your primary key columns are the same as the
154   * foreign key columns. For example, when the primary key columns are
155   * "ORDER_ID", "CUST_ID" etc ... and they are the same as the foreign key
156   * column names.
157   * </p>
158   */
159  @Override
160  public boolean isUseForeignKeyPrefix() {
161    return useForeignKeyPrefix;
162  }
163
164  /**
165   * Set this to false when the primary key columns matching your foreign key
166   * columns.
167   */
168  public void setUseForeignKeyPrefix(boolean useForeignKeyPrefix) {
169    this.useForeignKeyPrefix = useForeignKeyPrefix;
170  }
171
172  /**
173   * Return the tableName using the naming convention (rather than deployed
174   * Table annotation).
175   */
176  protected abstract TableName getTableNameByConvention(Class<?> beanClass);
177
178  /**
179   * Returns the table name for a given entity bean.
180   * <p>
181   * This first checks for the @Table annotation and if not present uses the
182   * naming convention to define the table name.
183   * </p>
184   *
185   * @see #getTableNameFromAnnotation(Class)
186   * @see #getTableNameByConvention(Class)
187   */
188  @Override
189  public TableName getTableName(Class<?> beanClass) {
190    while (true) {
191
192      TableName tableName = getTableNameFromAnnotation(beanClass);
193      if (tableName == null) {
194        Class<?> supCls = beanClass.getSuperclass();
195        if (hasInheritance(supCls)) {
196          // get the table as per inherited class in case there
197          // is not a table annotation in the inheritance hierarchy
198          beanClass = supCls;
199          continue;
200        }
201
202        tableName = getTableNameByConvention(beanClass);
203      }
204
205      // Use naming convention for catalog or schema,
206      // if not set in the annotation.
207      String catalog = tableName.getCatalog();
208      if (isEmpty(catalog)) {
209        catalog = getCatalog();
210      }
211      String schema = tableName.getSchema();
212      if (isEmpty(schema)) {
213        schema = getSchema();
214      }
215      return new TableName(catalog, schema, tableName.getName());
216    }
217  }
218
219  /**
220   * Return true if this class is part of entity inheritance.
221   */
222  protected boolean hasInheritance(Class<?> supCls) {
223    return AnnotationUtil.typeHas(supCls, Inheritance.class)
224        || AnnotationUtil.has(supCls, DiscriminatorValue.class);
225  }
226
227  @Override
228  public TableName getM2MJoinTableName(TableName lhsTable, TableName rhsTable) {
229    StringBuilder buffer = new StringBuilder();
230    buffer.append(unQuote(lhsTable.getName()));
231    buffer.append("_");
232
233    String rhsTableName = unQuote(rhsTable.getName());
234    if (rhsTableName.indexOf('_') < rhsPrefixLength) {
235      // trim off a xx_ prefix if there is one
236      rhsTableName = rhsTableName.substring(rhsTableName.indexOf('_') + 1);
237    }
238    buffer.append(rhsTableName);
239
240    int maxTableNameLength = databasePlatform.getMaxTableNameLength();
241
242    // maxConstraintNameLength is used as the max table name length.
243    if (buffer.length() > maxTableNameLength) {
244      buffer.setLength(maxTableNameLength);
245    }
246
247    String tableName = quoteIdentifiers(buffer.toString());
248    return new TableName(lhsTable.getCatalog(), lhsTable.getSchema(), tableName);
249  }
250
251  @Override
252  public String deriveM2MColumn(String tableName, String dbColumn) {
253    return quoteIdentifiers(unQuote(tableName) +"_" + unQuote(dbColumn));
254  }
255
256  /**
257   * Gets the table name from annotation.
258   */
259  protected TableName getTableNameFromAnnotation(Class<?> beanClass) {
260    final Table table = AnnotationUtil.typeGet(beanClass, Table.class);
261    if (table != null && !isEmpty(table.name())) {
262      // Note: empty catalog and schema are converted to null
263      // Only need to convert quoted identifiers from annotations
264      return new TableName(quoteIdentifiers(table.catalog()), quoteIdentifiers(table.schema()), quoteIdentifiers(table.name()));
265    }
266    // No annotation
267    return null;
268  }
269
270  @Override
271  public String getTableName(String catalog, String schema, String name) {
272    StringBuilder sb = new StringBuilder();
273    if (!isNull(catalog)) {
274      sb.append(quoteIdentifiers(catalog)).append(".");
275    }
276    if (!isNull(schema)) {
277      sb.append(quoteIdentifiers(schema)).append(".");
278    }
279    return sb.append(quoteIdentifiers(name)).toString();
280  }
281
282  /**
283   * Replace back ticks (if they are used) with database platform specific quoted identifiers.
284   */
285  protected String quoteIdentifiers(String s) {
286    return databasePlatform.convertQuotedIdentifiers(s);
287  }
288
289  private String unQuote(String val) {
290    return databasePlatform.unQuote(val);
291  }
292
293  /**
294   * Checks string is null or empty .
295   */
296  protected boolean isEmpty(String s) {
297    return s == null || s.trim().isEmpty();
298  }
299
300  /**
301   * Load settings from properties.
302   */
303  @Override
304  public void loadFromProperties(PropertiesWrapper properties) {
305
306    useForeignKeyPrefix = properties.getBoolean("namingConvention.useForeignKeyPrefix", useForeignKeyPrefix);
307    sequenceFormat = properties.get("namingConvention.sequenceFormat", sequenceFormat);
308    schema = properties.get("namingConvention.schema", schema);
309  }
310
311}