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