View Javadoc
1   /*
2    *    Copyright 2006-2026 the original author or authors.
3    *
4    *    Licensed under the Apache License, Version 2.0 (the "License");
5    *    you may not use this file except in compliance with the License.
6    *    You may obtain a copy of the License at
7    *
8    *       https://www.apache.org/licenses/LICENSE-2.0
9    *
10   *    Unless required by applicable law or agreed to in writing, software
11   *    distributed under the License is distributed on an "AS IS" BASIS,
12   *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *    See the License for the specific language governing permissions and
14   *    limitations under the License.
15   */
16  package org.mybatis.generator.api;
17  
18  import static org.mybatis.generator.internal.util.StringUtility.composeFullyQualifiedTableName;
19  import static org.mybatis.generator.internal.util.StringUtility.mapStringValueOrElseGet;
20  import static org.mybatis.generator.internal.util.StringUtility.stringHasValue;
21  import static org.mybatis.generator.internal.util.StringUtility.stringValueOrElseGet;
22  
23  import java.util.Objects;
24  import java.util.Optional;
25  import java.util.regex.Matcher;
26  import java.util.regex.Pattern;
27  
28  import org.jspecify.annotations.Nullable;
29  import org.mybatis.generator.config.Context;
30  import org.mybatis.generator.config.DomainObjectRenamingRule;
31  import org.mybatis.generator.internal.util.JavaBeansUtil;
32  
33  public class FullyQualifiedTable {
34      private final @Nullable String introspectedCatalog;
35      private final @Nullable String introspectedSchema;
36      private final String introspectedTableName;
37      private final @Nullable String runtimeCatalog;
38      private final @Nullable String runtimeSchema;
39      private final @Nullable String runtimeTableName;
40      private @Nullable String configuredDomainObjectName;
41      private @Nullable String domainObjectSubPackage;
42      private final @Nullable String alias;
43      private final boolean ignoreQualifiersAtRuntime;
44      private final String beginningDelimiter;
45      private final String endingDelimiter;
46      private final @Nullable DomainObjectRenamingRule domainObjectRenamingRule;
47  
48      public FullyQualifiedTable(Builder builder) {
49          introspectedCatalog = builder.introspectedCatalog;
50          introspectedSchema = builder.introspectedSchema;
51          introspectedTableName = Objects.requireNonNull(builder.introspectedTableName);
52          ignoreQualifiersAtRuntime = builder.ignoreQualifiersAtRuntime;
53          runtimeCatalog = builder.runtimeCatalog;
54          runtimeSchema = builder.runtimeSchema;
55          runtimeTableName = builder.runtimeTableName;
56          domainObjectRenamingRule = builder.domainObjectRenamingRule;
57  
58          if (stringHasValue(builder.domainObjectName)) {
59              int index = builder.domainObjectName.lastIndexOf('.');
60              if (index == -1) {
61                  configuredDomainObjectName = builder.domainObjectName;
62              } else {
63                  configuredDomainObjectName = builder.domainObjectName.substring(index + 1);
64                  domainObjectSubPackage = builder.domainObjectName.substring(0, index);
65              }
66          }
67  
68          if (builder.alias == null) {
69              alias = null;
70          } else {
71              this.alias = builder.alias.trim();
72          }
73  
74          if (builder.delimitIdentifiers && builder.context != null) {
75              beginningDelimiter =  builder.context.getBeginningDelimiter();
76              endingDelimiter = builder.context.getEndingDelimiter();
77          } else {
78              beginningDelimiter = ""; //$NON-NLS-1$
79              endingDelimiter = ""; //$NON-NLS-1$
80  
81          }
82      }
83  
84      public Optional<String> getIntrospectedCatalog() {
85          return Optional.ofNullable(introspectedCatalog);
86      }
87  
88      public Optional<String> getIntrospectedSchema() {
89          return Optional.ofNullable(introspectedSchema);
90      }
91  
92      public String getIntrospectedTableName() {
93          return introspectedTableName;
94      }
95  
96      public String getFullyQualifiedTableNameAtRuntime() {
97          StringBuilder localCatalog = new StringBuilder();
98          if (!ignoreQualifiersAtRuntime) {
99              if (stringHasValue(runtimeCatalog)) {
100                 localCatalog.append(runtimeCatalog);
101             } else if (stringHasValue(introspectedCatalog)) {
102                 localCatalog.append(introspectedCatalog);
103             }
104         }
105         if (!localCatalog.isEmpty()) {
106             addDelimiters(localCatalog);
107         }
108 
109         StringBuilder localSchema = new StringBuilder();
110         if (!ignoreQualifiersAtRuntime) {
111             if (stringHasValue(runtimeSchema)) {
112                 localSchema.append(runtimeSchema);
113             } else if (stringHasValue(introspectedSchema)) {
114                 localSchema.append(introspectedSchema);
115             }
116         }
117         if (!localSchema.isEmpty()) {
118             addDelimiters(localSchema);
119         }
120 
121         StringBuilder localTableName = new StringBuilder();
122         if (stringHasValue(runtimeTableName)) {
123             localTableName.append(runtimeTableName);
124         } else {
125             localTableName.append(introspectedTableName);
126         }
127         addDelimiters(localTableName);
128 
129         return composeFullyQualifiedTableName(localCatalog.toString(), localSchema.toString(),
130                 localTableName.toString(), '.');
131     }
132 
133     public String getAliasedFullyQualifiedTableNameAtRuntime() {
134         StringBuilder sb = new StringBuilder();
135 
136         sb.append(getFullyQualifiedTableNameAtRuntime());
137 
138         if (stringHasValue(alias)) {
139             sb.append(' ');
140             sb.append(alias);
141         }
142 
143         return sb.toString();
144     }
145 
146     public String getDomainObjectName() {
147         return stringValueOrElseGet(configuredDomainObjectName, this::calculateDomainObjectName);
148     }
149 
150     private String calculateDomainObjectName() {
151         String finalDomainObjectName = mapStringValueOrElseGet(runtimeTableName,
152                 s -> JavaBeansUtil.getCamelCaseString(s, true),
153                 () -> JavaBeansUtil.getCamelCaseString(introspectedTableName, true));
154 
155         if (domainObjectRenamingRule != null) {
156             Pattern pattern = domainObjectRenamingRule.pattern();
157             String replaceString = domainObjectRenamingRule.replaceString();
158             Matcher matcher = pattern.matcher(finalDomainObjectName);
159             finalDomainObjectName = JavaBeansUtil.getFirstCharacterUppercase(matcher.replaceAll(replaceString));
160         }
161         return finalDomainObjectName;
162     }
163 
164     @Override
165     public boolean equals(Object obj) {
166         if (this == obj) {
167             return true;
168         }
169 
170         if (!(obj instanceof FullyQualifiedTable other)) {
171             return false;
172         }
173 
174         return Objects.equals(this.introspectedTableName, other.introspectedTableName)
175                 && Objects.equals(this.introspectedCatalog, other.introspectedCatalog)
176                 && Objects.equals(this.introspectedSchema, other.introspectedSchema);
177     }
178 
179     @Override
180     public int hashCode() {
181         return Objects.hash(introspectedTableName, introspectedCatalog, introspectedCatalog);
182     }
183 
184     @Override
185     public String toString() {
186         return composeFullyQualifiedTableName(
187                 introspectedCatalog, introspectedSchema, introspectedTableName,
188                 '.');
189     }
190 
191     public Optional<String> getAlias() {
192         return Optional.ofNullable(alias);
193     }
194 
195     /**
196      * Calculates a Java package fragment based on the table catalog and schema.
197      * If qualifiers are ignored, then this method will return an empty string.
198      *
199      * <p>This method is used for determining the sub package for Java client and
200      * SQL map (XML) objects.  It ignores any sub-package added to the
201      * domain object name in the table configuration.
202      *
203      * @param isSubPackagesEnabled
204      *            the is sub packages enabled
205      * @return the subpackage for this table
206      */
207     public String getSubPackageForClientOrSqlMap(boolean isSubPackagesEnabled) {
208         StringBuilder sb = new StringBuilder();
209         if (!ignoreQualifiersAtRuntime && isSubPackagesEnabled) {
210             if (stringHasValue(runtimeCatalog)) {
211                 sb.append('.');
212                 sb.append(runtimeCatalog.toLowerCase());
213             } else if (stringHasValue(introspectedCatalog)) {
214                 sb.append('.');
215                 sb.append(introspectedCatalog.toLowerCase());
216             }
217 
218             if (stringHasValue(runtimeSchema)) {
219                 sb.append('.');
220                 sb.append(runtimeSchema.toLowerCase());
221             } else if (stringHasValue(introspectedSchema)) {
222                 sb.append('.');
223                 sb.append(introspectedSchema.toLowerCase());
224             }
225         }
226 
227         // TODO - strip characters that are not valid in package names
228         return sb.toString();
229     }
230 
231     /**
232      * Calculates a Java package fragment based on the table catalog and schema.
233      * If qualifiers are ignored, then this method will return an empty string.
234      *
235      * <p>This method is used for determining the sub package for Java model objects only.
236      * It takes into account the possibility that a sub-package was added to the
237      * domain object name in the table configuration.
238      *
239      * @param isSubPackagesEnabled
240      *            the is sub packages enabled
241      * @return the subpackage for this table
242      */
243     public String getSubPackageForModel(boolean isSubPackagesEnabled) {
244         StringBuilder sb = new StringBuilder();
245         sb.append(getSubPackageForClientOrSqlMap(isSubPackagesEnabled));
246 
247         if (stringHasValue(domainObjectSubPackage)) {
248             sb.append('.');
249             sb.append(domainObjectSubPackage);
250         }
251 
252         return sb.toString();
253     }
254 
255     private void addDelimiters(StringBuilder sb) {
256         if (stringHasValue(beginningDelimiter)) {
257             sb.insert(0, beginningDelimiter);
258         }
259 
260         if (stringHasValue(endingDelimiter)) {
261             sb.append(endingDelimiter);
262         }
263     }
264 
265     public Optional<String> getDomainObjectSubPackage() {
266         return Optional.ofNullable(domainObjectSubPackage);
267     }
268 
269     public static class Builder {
270         private @Nullable String introspectedCatalog;
271         private @Nullable String introspectedSchema;
272         private @Nullable String introspectedTableName;
273         private @Nullable String runtimeCatalog;
274         private @Nullable String runtimeSchema;
275         private @Nullable String runtimeTableName;
276         private @Nullable String domainObjectName;
277         private @Nullable String alias;
278         private boolean ignoreQualifiersAtRuntime;
279         private boolean delimitIdentifiers;
280         private @Nullable DomainObjectRenamingRule domainObjectRenamingRule;
281         private @Nullable Context context;
282 
283         /**
284          * Sets the actual catalog of the table as returned from DatabaseMetaData.
285          *
286          * <p>This value should only be set if the user configured a catalog. Otherwise, the
287          * DatabaseMetaData is reporting some database default that we don't want in the generated code.
288          *
289          * @param introspectedCatalog the introspected catalog
290          * @return this builder
291          */
292         public Builder withIntrospectedCatalog(@Nullable String introspectedCatalog) {
293             this.introspectedCatalog = introspectedCatalog;
294             return this;
295         }
296 
297         /**
298          * Sets the actual schema of the table as returned from DatabaseMetaData.
299          *
300          * <p>This value should only be set if the user configured a schema. Otherwise, the
301          * DatabaseMetaData is reporting some database default that we don't want in the generated code.
302          *
303          * @param introspectedSchema the introspected schema
304          * @return this builder
305          */
306         public Builder withIntrospectedSchema(@Nullable String introspectedSchema) {
307             this.introspectedSchema = introspectedSchema;
308             return this;
309         }
310 
311         /**
312          * Sets the actual table name as returned from DatabaseMetaData.
313          *
314          * @param introspectedTableName the introspected table name
315          * @return this builder
316          */
317         public Builder withIntrospectedTableName(String introspectedTableName) {
318             this.introspectedTableName = introspectedTableName;
319             return this;
320         }
321 
322         /**
323          * Sets the runtime catalog.
324          *
325          * <p>This is used to "rename" the catalog in the generated SQL. This is useful, for example, when
326          *    generating code against one catalog that should run with a different catalog.
327          *
328          * @param runtimeCatalog the runtime catalog
329          * @return this builder
330          */
331         public Builder withRuntimeCatalog(@Nullable String runtimeCatalog) {
332             this.runtimeCatalog = runtimeCatalog;
333             return this;
334         }
335 
336         /**
337          * Sets the runtime schema.
338          *
339          * <p>This is used to "rename" the schema in the generated SQL. This is useful, for example, when
340          *    generating code against one schema that should run with a different schema.
341          *
342          * @param runtimeSchema the runtime schema
343          * @return this builder
344          */
345         public Builder withRuntimeSchema(@Nullable String runtimeSchema) {
346             this.runtimeSchema = runtimeSchema;
347             return this;
348         }
349 
350         /**
351          * Sets the runtime table name.
352          *
353          * <p>This is used to "rename" the table in the generated SQL. This is useful, for example, when generating
354          *    code to run with an Oracle synonym. The user would have to specify the actual table name and schema
355          *    for generation, but would want to use the synonym name in the generated SQL
356          *
357          * @param runtimeTableName the runtime table name
358          * @return this builder
359          */
360         public Builder withRuntimeTableName(@Nullable String runtimeTableName) {
361             this.runtimeTableName = runtimeTableName;
362             return this;
363         }
364 
365         /**
366          * Set the configured domain object name for this table.
367          *
368          * <p>If nothing is configured, we'll build the domain object named based on the tableName or runtimeTableName.
369          *
370          * @param domainObjectName the domain object name
371          * @return this builder
372          */
373         public Builder withDomainObjectName(@Nullable String domainObjectName) {
374             this.domainObjectName = domainObjectName;
375             return this;
376         }
377 
378         /**
379          * Sets a configured alias for the table. This alias will be added to the table name in the SQL.
380          *
381          * @param alias the alias
382          * @return this builder
383          */
384         public Builder withAlias(@Nullable String alias) {
385             this.alias = alias;
386             return this;
387         }
388 
389         /**
390          * If true, then the catalog and schema qualifiers will be ignored when composing fully qualified names
391          * in the generated SQL.
392          *
393          * <p>This is used, for example, when the user needs to specify a specific schema for
394          * generating code but does not want the schema in the generated SQL
395          *
396          * @param ignoreQualifiersAtRuntime whether to ignore qualifiers at runtime
397          * @return this builder
398          */
399         public Builder withIgnoreQualifiersAtRuntime(boolean ignoreQualifiersAtRuntime) {
400             this.ignoreQualifiersAtRuntime = ignoreQualifiersAtRuntime;
401             return this;
402         }
403 
404         /**
405          * If true, then the table identifiers will be delimited at runtime.
406          *
407          * <p>The delimiter characters are obtained from the Context.
408          *
409          * @param delimitIdentifiers whether to delimit identifiers at runtime
410          * @return this builder
411          */
412         public Builder withDelimitIdentifiers(boolean delimitIdentifiers) {
413             this.delimitIdentifiers = delimitIdentifiers;
414             return this;
415         }
416 
417         /**
418          * Sets a domain object renaming rule.
419          *
420          * <p>This is ignored is a domain object name is configured.
421          *
422          * <p>If a domain object name is not configured, we'll build the domain object named based on the tableName
423          * or runtimeTableName, and then we use the domain object renaming rule to generate the final domain object
424          * name.
425          *
426          * @param domainObjectRenamingRule the domain object renaming rule
427          * @return this builder
428          */
429         public Builder withDomainObjectRenamingRule(@Nullable DomainObjectRenamingRule domainObjectRenamingRule) {
430             this.domainObjectRenamingRule = domainObjectRenamingRule;
431             return this;
432         }
433 
434         public Builder withContext(Context context) {
435             this.context = context;
436             return this;
437         }
438 
439         public FullyQualifiedTable build() {
440             return new FullyQualifiedTable(this);
441         }
442     }
443 }