View Javadoc
1   /*
2    * Tentackle - https://tentackle.org.
3    *
4    * This library is free software; you can redistribute it and/or
5    * modify it under the terms of the GNU Lesser General Public
6    * License as published by the Free Software Foundation; either
7    * version 2.1 of the License, or (at your option) any later version.
8    *
9    * This library is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12   * Lesser General Public License for more details.
13   *
14   * You should have received a copy of the GNU Lesser General Public
15   * License along with this library; if not, write to the Free Software
16   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17   */
18  
19  package org.tentackle.persist.wurblet;
20  
21  import org.wurbelizer.wurbel.WurbelException;
22  
23  import org.tentackle.common.StringHelper;
24  import org.tentackle.model.Attribute;
25  import org.tentackle.model.AttributeSorting;
26  import org.tentackle.model.Entity;
27  import org.tentackle.model.InheritanceType;
28  import org.tentackle.model.MethodArgument;
29  import org.tentackle.model.ModelException;
30  import org.tentackle.model.Relation;
31  import org.tentackle.model.RelationType;
32  import org.tentackle.model.SelectionType;
33  import org.tentackle.model.SortType;
34  import org.tentackle.model.TrackType;
35  import org.tentackle.sql.Backend;
36  import org.tentackle.sql.DataType;
37  import org.tentackle.wurblet.Join;
38  import org.tentackle.wurblet.JoinPath;
39  import org.tentackle.wurblet.ModelWurblet;
40  import org.tentackle.wurblet.WurbletArgument;
41  import org.tentackle.wurblet.WurbletArgumentExpression;
42  import org.tentackle.wurblet.WurbletArgumentParser;
43  import org.tentackle.wurblet.WurbletRelation;
44  
45  import java.util.ArrayList;
46  import java.util.HashSet;
47  import java.util.LinkedHashSet;
48  import java.util.List;
49  import java.util.Set;
50  import java.util.StringTokenizer;
51  import java.util.regex.Pattern;
52  
53  
54  /**
55   * Base class for persistence wurblets.
56   * <p>
57   * The following wurblet options are supported:
58   * <ul>
59   * <li><em>--context=&lt;key&gt;:</em> defines the context id (--context= turns off default context).</li>
60   * <li><em>--tracked:</em> return a TrackedList instead of List (useful if [TRACKED] not given in the model).</li>
61   * <li><em>--attracked:</em> same as --tracked but generate is...Modified per attribute.</li>
62   * <li><em>--fulltracked:</em> same as --attracked but keep last persisted values as well.</li>
63   * <li><em>--untracked:</em> disable [TRACKED, ATTRACKED, or FULLTRACKED] from the model.</li>
64   * </ul>
65   * For more options, see {@link ModelWurblet}.
66   *
67   * @author harald
68   */
69  public class DbModelWurblet extends ModelWurblet {
70  
71    /**
72     * Java code conventions for constants: an optionally qualified name with the last segment in uppercase.
73     */
74    private static final Pattern CONSTANT_PATTERN = Pattern.compile("(?:[A-Za-z_$][A-Za-z0-9_$]*\\.)*[A-Z][A-Z0-9_]*");
75  
76    private boolean argumentGroupingEnabled;
77    private boolean pathAllowed;
78    private boolean tracked;
79    private boolean attracked;
80    private boolean fullTracked;
81    private Attribute contextAttribute;
82    private List<JoinPath> joinPaths;
83    private WurbletArgumentParser parser;
84    private List<WurbletArgument> methodArguments;
85  
86    /**
87     * Returns whether argument grouping is enabled.<br>
88     * By default, the grouping separator '|' just improves readability.<br>
89     * Some wurblet, however, need extra arguments separated from the main expression.
90     * This is true for the PdoUpdateBy or DbUpdateBy wurblets, for example.
91     * Grouping is turned on via the wurblet option "groupArgs".
92     *
93     * @return true if grouping enabled
94     */
95    public boolean isArgumentGroupingEnabled() {
96      return argumentGroupingEnabled;
97    }
98  
99    /**
100    * Returns whether expression arguments may contain paths to other entities.
101    *
102    * @return true if allowed, false if not
103    */
104   public boolean isPathAllowed() {
105     return pathAllowed;
106   }
107 
108 
109   @Override
110   public void run() throws WurbelException {
111 
112     String wurbletOptions = getConfiguration();
113     if (wurbletOptions != null) {
114       if (wurbletOptions.contains("groupArgs")) {
115         argumentGroupingEnabled = true;
116       }
117       if (wurbletOptions.contains("pathAllowed")) {
118         pathAllowed = true;
119       }
120     }
121 
122     super.run();
123 
124     Entity entity = getEntity();
125     if (entity != null) {   // null if missingModelOk set and no such entity in model (see ModelWurblet)
126 
127       applyTrackingAndContextOptions(entity);
128 
129       // analyze the wurblet arguments
130       parser = new WurbletArgumentParser(entity, argumentGroupingEnabled, getWurbletArgs());
131 
132       if (!pathAllowed) {
133         for (WurbletArgument arg: parser.getAllArguments()) {
134           if (arg.isPath()) {
135             throw new WurbelException("relation paths not allowed in wurblet " + this);
136           }
137         }
138       }
139 
140       // optional conditions are generated for the where-expression only, not for the filters of load joins
141       for (WurbletArgument arg: getMethodArguments()) {
142         if (arg.isOptional() && !parser.getExpressionArguments().contains(arg)) {
143           throw new WurbelException("optional condition '" + arg + "' not allowed in a join filter");
144         }
145       }
146 
147       if (isRemote()) {
148         // check whether relations are not transient
149         for (WurbletArgument joinArgument: parser.getJoinArguments()) {
150           List<WurbletRelation> wurbletRelations = joinArgument.getWurbletRelations();
151           if (wurbletRelations != null) {   // is always != null, but one never knows ...
152             for (WurbletRelation wurbletRelation : wurbletRelations) {
153               Relation relation = wurbletRelation.getRelation();
154               if (!relation.isComposite() && !relation.isSerialized() && relation.getSelectionType() != SelectionType.EAGER) {
155                 throw new WurbelException("joined non-composite relation '" + relation.getName() +
156                                           "' must be serialized for remote access");
157               }
158               Relation opRel = relation.getForeignRelation();
159               if (opRel != null && !opRel.isSerialized() && !opRel.isComposite()) {
160                 throw new WurbelException("joined opposite relation '" + opRel.getEntity() + "." + opRel.getName() +
161                                           "' must be serialized for remote access");
162               }
163             }
164           }
165           else {
166             throw new WurbelException("join argument '" + joinArgument + "' does not provide relations?");
167           }
168         }
169       }
170     }
171 
172   }
173 
174 
175   /**
176    * Applies the tracking and context options.<br>
177    * The defaults are taken from the entity's model and overridden by the options
178    * {@code --tracked}, {@code --attracked}, {@code --fulltracked}, {@code --untracked} and {@code --context=<key>}.
179    *
180    * @param entity the entity
181    * @throws WurbelException if the context attribute does not exist
182    */
183   protected void applyTrackingAndContextOptions(Entity entity) throws WurbelException {
184     tracked = entity.getOptions().getTrackType().isTracked();
185     attracked = entity.getOptions().getTrackType().isAttracked();
186     fullTracked = entity.getOptions().getTrackType() == TrackType.FULLTRACKED;
187     contextAttribute = entity.getContextIdAttribute();
188 
189     for (String arg: getOptionArgs())  {
190       if ("tracked".equals(arg)) {
191         tracked = true;
192         attracked = false;
193         fullTracked = false;
194       }
195       else if ("attracked".equals(arg)) {
196         tracked = true;
197         attracked = true;
198         fullTracked = false;
199       }
200       else if ("fulltracked".equals(arg)) {
201         tracked = true;
202         attracked = true;
203         fullTracked = true;
204       }
205       else if ("untracked".equals(arg)) {
206         tracked = false;
207         attracked = false;
208         fullTracked = false;
209       }
210       else if (arg.startsWith("context="))  {
211         String ctx = arg.substring(8);
212         if (ctx.isEmpty()) {
213           contextAttribute = null;
214         }
215         else {
216           // a typo must not silently drop the context condition
217           contextAttribute = entity.getAttributeByJavaName(ctx, true);
218           if (contextAttribute == null) {
219             throw new WurbelException("no such context attribute '" + ctx + "' in " + entity);
220           }
221         }
222       }
223     }
224   }
225 
226   /**
227    * Returns whether the entity is tracked.
228    *
229    * @return true if tracked (TRACKED, ATTRACKED or FULLTRACKED)
230    */
231   public boolean isTracked() {
232     return tracked;
233   }
234 
235   /**
236    * Returns whether the entity is attracked or fulltracked.
237    *
238    * @return true if attracked (ATTRACKED or FULLTRACKED)
239    */
240   public boolean isAttracked() {
241     return attracked;
242   }
243 
244   /**
245    * Returns whether the entity is fulltracked.
246    *
247    * @return true if FULLTRACKED
248    */
249   public boolean isFullTracked() {
250     return fullTracked;
251   }
252 
253   /**
254    * Gets the context attribute.
255    *
256    * @return the context attribute, null if none
257    */
258   public Attribute getContextAttribute() {
259     return contextAttribute;
260   }
261 
262 
263   /**
264    * Returns whether SQL statements can be executed for that entity.
265    *
266    * @return true if persistable
267    */
268   public boolean isEntityPersistable() {
269     return !getEntity().isAbstract() ||
270            !getEntity().getHierarchyInheritanceType().isMappingToNoTable();
271   }
272 
273   /**
274    * Asserts that SQL statements can be executed for that entity.
275    *
276    * @throws WurbelException if not
277    */
278   public void assertEntityIsPersistable() throws WurbelException {
279     if (!isEntityPersistable()) {
280       throw new WurbelException(getEntity() + " does not map to any table and is not persistable");
281     }
282   }
283 
284   /**
285    * Asserts that entity can be selected from the database.
286    *
287    * @throws WurbelException if not
288    */
289   public void assertEntityNotEmbedded() throws WurbelException {
290     if (getEntity().isEmbedded()) {
291       StringBuilder buf = new StringBuilder();
292       for (Entity embeddingEntity : getEntity().getEmbeddingEntities()) {
293         if (!buf.isEmpty()) {
294           buf.append(", ");
295         }
296         buf.append(embeddingEntity);
297       }
298       throw new WurbelException(getEntity() + " is embedded in " + buf);
299     }
300   }
301 
302 
303   /**
304    * Gets the method arguments.
305    *
306    * @return the method arguments
307    */
308   public List<WurbletArgument> getMethodArguments() {
309     if (methodArguments == null) {
310       List<WurbletArgument> wurbletArguments = new ArrayList<>();
311       for (JoinPath joinPath : getJoinPaths()) {
312         for (Join element : joinPath.getElements()) {
313           WurbletArgumentParser elementParser = element.getWurbletRelation().getParser();
314           if (elementParser != null) {
315             wurbletArguments.addAll(elementParser.getMethodArguments());
316           }
317         }
318       }
319       wurbletArguments.addAll(parser.getMethodArguments());
320       methodArguments = wurbletArguments;
321     }
322     return methodArguments;
323   }
324 
325 
326   /**
327    * Gets the expression arguments.
328    *
329    * @return the arguments used within the expression
330    */
331   public List<WurbletArgument> getExpressionArguments() {
332     return parser.getExpressionArguments();
333   }
334 
335 
336   /**
337    * Returns whether at least one condition is optional.<br>
338    * Because the SQL text of such finders depends on the boolean method arguments,
339    * the prepared statement cannot be cached, i.e. the option {@code --oneshot} is implied.
340    *
341    * @return true if there are optional conditions
342    * @see WurbletArgument#isOptional()
343    */
344   public boolean isWithOptionalArguments() {
345     return parser.isWithOptionalArguments();
346   }
347 
348 
349   /**
350    * Asserts that none of the conditions is optional.<br>
351    * To be invoked by wurblets caching their prepared statement, since the SQL text
352    * of an optional condition is not fixed.
353    *
354    * @throws WurbelException if there is an optional condition
355    */
356   public void assertNoOptionalArguments() throws WurbelException {
357     for (WurbletArgument arg: getMethodArguments()) {
358       if (arg.isOptional()) {
359         throw new WurbelException("optional condition '" + arg + "' not supported by wurblet " + this);
360       }
361     }
362   }
363 
364 
365   /**
366    * Creates the java boolean expression that determines whether the given arguments
367    * are part of the query.<br>
368    * The expression is true if at least one of the arguments is applicable.
369    * Arguments referring to the same boolean method argument are considered only once.
370    *
371    * @param args the wurblet arguments
372    * @return the Java code, null if the arguments are always part of the query
373    */
374   public String createOptionalCondition(List<WurbletArgument> args) {
375     StringBuilder buf = new StringBuilder();
376     Set<String> names = new LinkedHashSet<>();
377     for (WurbletArgument arg: args) {
378       if (!arg.isOptional()) {
379         return null;    // mandatory condition: always applicable
380       }
381       names.add(arg.getOptionalName());
382     }
383     for (String name: names) {
384       if (!buf.isEmpty()) {
385         buf.append(" || ");
386       }
387       buf.append(name);
388     }
389     return buf.isEmpty() ? null : buf.toString();
390   }
391 
392 
393   /**
394    * Gets the extra arguments.<br>
395    * Those are not part of the expression and not sorting arguments.
396    *
397    * @return the extra arguments
398    */
399   public List<WurbletArgument> getExtraArguments() {
400     return parser.getExtraArguments();
401   }
402 
403   /**
404    * Gets the select/where expression.
405    *
406    * @return the expression
407    */
408   public WurbletArgumentExpression getExpression() {
409     return parser.getExpression();
410   }
411 
412   /**
413    * Gets the sorting arguments.
414    *
415    * @return the sorting arguments, empty if no "order by"
416    */
417   public List<WurbletArgument> getSortingArguments() {
418     return parser.getSortingArguments();
419   }
420 
421   /**
422    * Returns whether sorting is configured for this wurblet.
423    *
424    * @return true if sorting defined in args
425    */
426   public boolean isWithSorting() {
427     return !getSortingArguments().isEmpty();
428   }
429 
430 
431   /**
432    * Goes up the inheritance tree until a default sorting is found.
433    *
434    * @return the sorting, null if none
435    */
436   public List<AttributeSorting> getDefaultSorting() {
437     Entity entity = getEntity();
438     while (entity != null) {
439       List<AttributeSorting> sorting = entity.getSorting();
440       if (sorting != null && !sorting.isEmpty()) {
441         return sorting;
442       }
443       entity = entity.getSuperEntity();
444     }
445     return null;
446   }
447 
448 
449   /**
450    * Gets the default sorting keys.
451    *
452    * @return the sorting keys, empty if no "order by"
453    * @throws WurbelException if failed
454    */
455   public List<WurbletArgument> getDefaultSortKeys() throws WurbelException {
456     List<WurbletArgument> sortKeys = new ArrayList<>();
457     List<AttributeSorting> sorting = getDefaultSorting();
458     if (sorting != null) {
459       for (AttributeSorting as: sorting) {
460         WurbletArgument key = parser.createArgument(as.toString(), true, false);
461         sortKeys.add(key);
462       }
463     }
464     return sortKeys;
465   }
466 
467 
468   /**
469    * Returns whether default sorting is configured for the entity.
470    *
471    * @return true if sorting enabled
472    */
473   public boolean isWithDefaultSorting() {
474     return getDefaultSorting() != null;
475   }
476 
477 
478 
479   /**
480    * Gets the consolidated relation paths for the join arguments.
481    *
482    * @return the join paths
483    */
484   public List<JoinPath> getJoinPaths() {
485     if (joinPaths == null) {
486       joinPaths = parser.getJoinPaths();
487       // normalize paths (simplifies code generation with JoinedSelects below)
488       for (JoinPath path: joinPaths) {
489         path.normalize();
490       }
491     }
492     return joinPaths;
493   }
494 
495   /**
496    * Returns whether there are load joins.
497    *
498    * @return true if with load joins
499    */
500   public boolean isWithJoins() {
501     return !getJoinPaths().isEmpty();
502   }
503 
504   /**
505    * Returns whether there are load joins with filters.
506    *
507    * @param componentsOnly true if check component relations only
508    * @return true if the result must be made immutable, false if no filters
509    */
510   public boolean isWithFilteredJoins(boolean componentsOnly) {
511     for (JoinPath joinPath : getJoinPaths()) {
512       if (joinPath.isFiltered(componentsOnly)) {
513         return true;
514       }
515     }
516     return false;
517   }
518 
519 
520   /**
521    * Creates the relational operation code for prepared statements.<br>
522    * The code assumes a StringBuilder and begins with <code>".append("</code>.
523    *
524    * @param arg the wurblet argument
525    * @return the Java code
526    */
527   public String createRelopCode(WurbletArgument arg) throws WurbelException {
528 
529     if (arg.isArray()) {
530       // this feature may not be supported by all backends
531       String op = arg.getArrayOperator();
532       assertSupportedByBackends("array operator '" + op + "'", b -> b.isArrayOperatorSupported(op));
533     }
534 
535     String value = arg.getValue();
536     if (arg.isValueLiterally() && !value.startsWith("\"")) {
537       value = StringHelper.toDoubleQuotes(value);
538     }
539 
540     String str;
541 
542     // translate to predefined strings, if possible
543     switch(arg.getRelop()) {
544       case Backend.SQL_EQUAL:
545         if (arg.isValueLiterally()) {
546           str = "Backend.SQL_EQUAL).append(" + value;
547         }
548         else  {
549           str = arg.isArray() ? "Backend.SQL_EQUAL" : "Backend.SQL_EQUAL_PAR";
550         }
551         break;
552 
553       case Backend.SQL_NOTEQUAL:
554       case "!=":
555         if (arg.isValueLiterally()) {
556           str = "Backend.SQL_NOTEQUAL).append(" + value;
557         }
558         else  {
559           str = arg.isArray() ? "Backend.SQL_NOTEQUAL" : "Backend.SQL_NOTEQUAL_PAR";
560         }
561         break;
562 
563       case Backend.SQL_LESS:
564         if (arg.isValueLiterally()) {
565           str = "Backend.SQL_LESS).append(" + value;
566         }
567         else  {
568           str = arg.isArray() ? "Backend.SQL_LESS" : "Backend.SQL_LESS_PAR";
569         }
570         break;
571 
572       case Backend.SQL_GREATER:
573         if (arg.isValueLiterally()) {
574           str = "Backend.SQL_GREATER).append(" + value;
575         }
576         else  {
577           str = arg.isArray() ? "Backend.SQL_GREATER" : "Backend.SQL_GREATER_PAR";
578         }
579         break;
580 
581       case Backend.SQL_LESSOREQUAL:
582         if (arg.isValueLiterally()) {
583           str = "Backend.SQL_LESSOREQUAL).append(" + value;
584         }
585         else  {
586           str = arg.isArray() ? "Backend.SQL_LESSOREQUAL" : "Backend.SQL_LESSOREQUAL_PAR";
587         }
588         break;
589 
590       case Backend.SQL_GREATEROREQUAL:
591         if (arg.isValueLiterally()) {
592           str = "Backend.SQL_GREATEROREQUAL).append(" + value;
593         }
594         else  {
595           str = arg.isArray() ? "Backend.SQL_GREATEROREQUAL" : "Backend.SQL_GREATEROREQUAL_PAR";
596         }
597         break;
598 
599       case Backend.SQL_LIKE:
600         if (arg.isValueLiterally()) {
601           str = "Backend.SQL_LIKE).append(" + value;
602         }
603         else  {
604           str = "Backend.SQL_LIKE_PAR";
605         }
606         break;
607 
608       case Backend.SQL_NOTLIKE:
609         if (arg.isValueLiterally()) {
610           str = "Backend.SQL_NOTLIKE).append(" + value;
611         }
612         else  {
613           str = "Backend.SQL_NOTLIKE_PAR";
614         }
615         break;
616 
617       case Backend.SQL_ISNULL:
618         str = "Backend.SQL_ISNULL";
619         break;
620 
621       case Backend.SQL_ISNOTNULL:
622         str = "Backend.SQL_ISNOTNULL";
623         break;
624 
625       default:
626         if (arg.isArray()) {
627           str = "";
628         }
629         else {
630           str = "\"" + arg.getRelop() + (arg.isValueLiterally() ? value : "?") + "\"";
631         }
632     }
633 
634     if (!str.isEmpty()) {
635       str = ".append(" + str + ")";
636     }
637 
638     if (arg.isArray()) {
639       switch (arg.getArrayOperator()) {
640         case Backend.SQL_ARRAY_ANY:
641           str += ".append(Backend.SQL_ARRAY_ANY_PAR)";
642           break;
643         case Backend.SQL_ARRAY_ALL:
644           str += ".append(Backend.SQL_ARRAY_ALL_PAR)";
645           break;
646         case Backend.SQL_ARRAY_NOT_IN:
647           str += ".append(Backend.SQL_ARRAY_NOT_IN_PAR)";  // includes leading space
648           break;
649         default:
650           str += ".append(Backend.SQL_ARRAY_IN_PAR)";    // includes leading space
651       }
652     }
653 
654     return str;
655   }
656 
657 
658   /**
659    * Creates the relation operation SQL for prepared statements.<br>
660    * Same as {@link #createRelopCode(WurbletArgument)}, but creates SQL code.
661    *
662    * @param arg the wurblet argument
663    * @return the SQL code
664    */
665   public String createRelopSql(WurbletArgument arg) throws WurbelException {
666 
667     if (arg.isArray()) {
668       // this feature may not be supported by all backends
669       String op = arg.getArrayOperator();
670       assertSupportedByBackends("array operator '" + op + "'", b -> b.isArrayOperatorSupported(op));
671     }
672 
673     String value = arg.getValue();
674     if (arg.isValueLiterally() && !value.startsWith("\"")) {
675       value = StringHelper.toDoubleQuotes(value);
676     }
677 
678     String str;
679 
680     // translate to predefined strings, if possible
681     switch(arg.getRelop()) {
682       case Backend.SQL_EQUAL:
683         if (arg.isValueLiterally()) {
684           str = Backend.SQL_EQUAL + value;
685         }
686         else  {
687           str = arg.isArray() ? Backend.SQL_EQUAL : Backend.SQL_EQUAL_PAR;
688         }
689         break;
690 
691       case Backend.SQL_NOTEQUAL:
692       case "!=":
693         if (arg.isValueLiterally()) {
694           str = Backend.SQL_NOTEQUAL + value;
695         }
696         else  {
697           str = arg.isArray() ? Backend.SQL_NOTEQUAL : Backend.SQL_NOTEQUAL_PAR;
698         }
699         break;
700 
701       case Backend.SQL_LESS:
702         if (arg.isValueLiterally()) {
703           str = Backend.SQL_LESS + value;
704         }
705         else  {
706           str = arg.isArray() ? Backend.SQL_LESS : Backend.SQL_LESS_PAR;
707         }
708         break;
709 
710       case Backend.SQL_GREATER:
711         if (arg.isValueLiterally()) {
712           str = Backend.SQL_GREATER + value;
713         }
714         else  {
715           str = arg.isArray() ? Backend.SQL_GREATER : Backend.SQL_GREATER_PAR;
716         }
717         break;
718 
719       case Backend.SQL_LESSOREQUAL:
720         if (arg.isValueLiterally()) {
721           str = Backend.SQL_LESSOREQUAL + value;
722         }
723         else  {
724           str = arg.isArray() ? Backend.SQL_LESSOREQUAL : Backend.SQL_LESSOREQUAL_PAR;
725         }
726         break;
727 
728       case Backend.SQL_GREATEROREQUAL:
729         if (arg.isValueLiterally()) {
730           str = Backend.SQL_GREATEROREQUAL + value;
731         }
732         else  {
733           str = arg.isArray() ? Backend.SQL_GREATEROREQUAL : Backend.SQL_GREATEROREQUAL_PAR;
734         }
735         break;
736 
737       case Backend.SQL_LIKE:
738         if (arg.isValueLiterally()) {
739           str = Backend.SQL_LIKE + value;
740         }
741         else  {
742           str = Backend.SQL_LIKE_PAR;
743         }
744         break;
745 
746       case Backend.SQL_NOTLIKE:
747         if (arg.isValueLiterally()) {
748           str = Backend.SQL_NOTLIKE + value;
749         }
750         else  {
751           str = Backend.SQL_NOTLIKE_PAR;
752         }
753         break;
754 
755       case Backend.SQL_ISNULL:
756         str = Backend.SQL_ISNULL;
757         break;
758 
759       case Backend.SQL_ISNOTNULL:
760         str = Backend.SQL_ISNOTNULL;
761         break;
762 
763       default:
764         if (arg.isArray()) {
765           str = "";
766         }
767         else {
768           str = arg.getRelop() + (arg.isValueLiterally() ? value : "?");
769         }
770     }
771 
772     if (arg.isArray()) {
773       switch (arg.getArrayOperator()) {
774         case Backend.SQL_ARRAY_ANY:
775           str += Backend.SQL_ARRAY_ANY_PAR;
776           break;
777         case Backend.SQL_ARRAY_ALL:
778           str += Backend.SQL_ARRAY_ALL_PAR;
779           break;
780         case Backend.SQL_ARRAY_NOT_IN:
781           str += Backend.SQL_ARRAY_NOT_IN_PAR;  // includes leading space
782           break;
783         default:
784           str += Backend.SQL_ARRAY_IN_PAR;    // includes leading space
785       }
786     }
787 
788     return str;
789   }
790 
791 
792   /**
793    * Creates the order by clause.
794    *
795    * @param sortKeys the sorting keys
796    * @return the order by clause, null if unsorted
797    * @throws WurbelException if failed
798    */
799   public String createOrderBy(List<WurbletArgument> sortKeys) throws WurbelException {
800     if (!sortKeys.isEmpty()) {
801       StringBuilder buf = new StringBuilder();
802       boolean needComma = false;
803       for (WurbletArgument key: sortKeys) {
804         int[] columnIndexes;
805         Attribute attr = key.getAttribute();
806         Integer index = key.getColumnIndex();
807         DataType<?> effectiveDataType = getEffectiveDataType(attr);
808         if (index != null) {
809           columnIndexes = new int[] { index };
810         }
811         else {
812           columnIndexes = effectiveDataType.getSortableColumns();
813         }
814         if (columnIndexes == null || columnIndexes.length == 0) {
815           throw new WurbelException(effectiveDataType + " is not sortable by the database");
816         }
817         for (int columnIndex : columnIndexes) {
818           if (needComma) {
819             buf.append("\n           .append(Backend.SQL_COMMA)");
820           }
821           String name = getColumnNameConstant(attr, columnIndex);
822           buf.append(".append(");
823           if (getEntity().getHierarchyInheritanceType() == InheritanceType.MULTI) {
824             buf.append(deriveClassNameForEntity(getEntity().getTopSuperEntity()))
825                .append(".CLASSVARIABLES.getColumnName(").append(name).append(')');
826           }
827           else  {
828             if (!getEntity().equals(attr.getEntity())) {
829               // must be a joined component (otherwise the column is not in the result set)
830               List<WurbletRelation> wurbletRelations = key.getWurbletRelations();
831               List<Relation> embeddingPath = key.getEmbeddingPath();
832               if (embeddingPath != null && !embeddingPath.isEmpty() && wurbletRelations != null && wurbletRelations.size() > embeddingPath.size()) {
833                 wurbletRelations = new ArrayList<>(wurbletRelations.subList(0, wurbletRelations.size() - embeddingPath.size()));
834               }
835               boolean joinFound = false;
836               for (JoinPath joinPath: getJoinPaths()) {
837                 Join join = joinPath.findJoin(wurbletRelations);
838                 if (join == null) {
839                   join = joinPath.findJoin(key.getComponent());
840                 }
841                 if (join != null) {
842                   buf.append('"').append(join.getName()).append('.');
843                   if (attr.isEmbedded()) {
844                     buf.append(key.getEmbeddingColumnPrefix()).append(attr.getColumnName());
845                   }
846                   else {
847                     buf.append(attr.getColumnName());
848                   }
849                   buf.append('"');
850                   joinFound = true;
851                   break;
852                 }
853               }
854               if (!joinFound) {
855                 if ((wurbletRelations == null || wurbletRelations.isEmpty()) && embeddingPath != null && !embeddingPath.isEmpty()) {
856                   buf.append('"').append(key.getEmbeddingColumnPrefix()).append(attr.getColumnName()).append('"');
857                 }
858                 else {
859                   throw new WurbelException("missing join for sort key: " + key);
860                 }
861               }
862             }
863             else {
864               if (isPdo()) {
865                 buf.append("getColumnName(").append(name).append(')');
866               }
867               else  {
868                 buf.append(name);
869               }
870             }
871           }
872           buf.append(").append(")
873              .append(SortType.ASC == key.getSortType() ? "Backend.SQL_SORTASC" : "Backend.SQL_SORTDESC")
874              .append(')');
875           needComma = true;
876         }
877       }
878       return buf.toString();
879     }
880     return null;
881   }
882 
883   /**
884    * Creates the order by clause.
885    *
886    * @return the order by clause, null if unsorted
887    * @throws WurbelException if failed
888    */
889   public String createOrderBy() throws WurbelException {
890     return createOrderBy(getSortingArguments());
891   }
892 
893 
894   /**
895    * Returns whether joins with delegates of abstract classes are used.<br>
896    * The casts of such joins are "unchecked" and need a SuppressWarnings to avoid Xlint warnings.
897    *
898    * @return true if join paths involve delegates of abstract classes
899    */
900   public boolean isAbstractJoinPath(List<JoinPath> paths) {
901     boolean isAbstract = false;
902     for (JoinPath path: paths) {
903       if (!path.getElements().isEmpty()) {
904         Join join = path.getElements().get(0);  // normalized, i.e., only one (see above)
905         Relation relation = join.getWurbletRelation().getRelation();
906         if (relation.getEntity().isAbstract() || relation.getForeignEntity().isAbstract()) {
907           isAbstract = true;
908           break;
909         }
910       }
911       if (isAbstractJoinPath(path.getPaths())) {
912         isAbstract = true;
913         break;
914       }
915     }
916     return isAbstract;
917   }
918 
919 
920   /**
921    * Creates the java code for {@code JoinedSelect}s.
922    *
923    * @return the generated Java code
924    * @throws WurbelException if failed
925    */
926   public String createJoins() throws WurbelException {
927     StringBuilder buf = new StringBuilder();
928     String pdoType = getPdoClassName();
929     if (isGenerified()) {
930       pdoType = "T";
931     }
932     buf.append("    JoinedSelect<").append(pdoType).append("> js = new JoinedSelect<")
933        .append(pdoType).append(">()\n");
934     createJoinsImpl(buf, getEntity(), "      ", getJoinPaths(), pdoType, null);
935     buf.replace(buf.length() - 1, buf.length(), ";\n");
936     return buf.toString();
937   }
938 
939   private void createJoinsImpl(StringBuilder buf, Entity lastNonEmbeddedEntity, String inset, List<JoinPath> paths, String pdoType, String parentName)
940           throws WurbelException {
941     for (JoinPath path: paths) {
942       String lastJoinName = null;
943       if (!path.getElements().isEmpty()) {
944         Join join = path.getElements().get(0);    // normalized, i.e., only one (see above)
945         Relation relation = join.getWurbletRelation().getRelation();
946         lastJoinName = join.getName();
947         String poImpl = deriveClassNameForEntity(relation.getEntity());
948         String alias = lastNonEmbeddedEntity.getTableProvidingEntity().getTableAlias();
949         String leftClass = relation.getEntity().equals(getEntity()) ? pdoType : lastNonEmbeddedEntity.getName();
950         String joinClass = relation.getForeignEntity().getName();
951         String joinAlias = relation.getForeignEntity().getTableProvidingEntity().getTableAlias();
952         buf.append(inset).append(".addJoin(\n");
953         if (relation.getRelationType() == RelationType.LIST) {
954           buf.append(inset).append("  new Join<>(JoinType.LEFT, ");
955           if (parentName == null) {
956             buf.append("getColumnName(CN_ID), \"");
957           }
958           else {
959             buf.append('"').append(parentName).append(".id\", \"");
960           }
961           buf.append(join.getName()).append('.').append(relation.getForeignAttribute().getColumnName())
962              .append("\", ").append(joinClass).append(".class, \"").append(join.getName()).append("\",\n");
963           String extraSql = createOptionalWhereForJoin(relation, parentName, join);
964           if (!extraSql.isEmpty()) {
965             buf.append(inset).append("             ").append(extraSql).append(",\n");
966           }
967           buf.append(inset).append("    (").append(leftClass).append(" ").append(alias).append(", ")
968              .append(joinClass).append(" ").append(joinAlias).append(") -> {\n");
969           if (relation.isReversed()) {
970             buf.append(inset).append("      ((").append(poImpl).append(") ").append(alias)
971                .append(".getPersistenceDelegate()).").append(relation.getSetterName());
972             if (relation.isSerialized()) {
973               buf.append("Blunt");
974             }
975             buf.append("(").append(joinAlias).append(");\n");
976           }
977           else {
978             buf.append(inset).append("      ((").append(poImpl).append(") ").append(alias)
979                .append(".getPersistenceDelegate()).").append(relation.getGetterName()).append("Blunt().addBlunt(")
980                .append(joinAlias).append(");\n");
981             if (relation.getLinkMethodName() != null) {
982               String joinImpl = deriveClassNameForEntity(relation.getForeignEntity());
983               buf.append(inset).append("      ((").append(joinImpl).append(") ")
984                  .append(joinAlias).append(".getPersistenceDelegate()).")
985                  .append(createRelationUpdateReferenceCode(relation, alias, true)).append(";\n");
986             }
987           }
988         }
989         else {
990           buf.append(inset).append("  new Join<>(JoinType.LEFT, ");
991           Attribute attribute = relation.getMethodArgs().getFirst().getAttribute();   // not relation.getAttribute!
992           if (parentName == null) {
993             buf.append("getColumnName(");
994             if (attribute.isEmbedded()) {
995               buf.append('"').append(join.getArgument().getEmbeddingColumnPrefix()).append(attribute.getColumnName()).append('"');
996             }
997             else {
998               buf.append(getColumnNameConstant(attribute, 0));
999             }
1000             buf.append("), \"");
1001           }
1002           else {
1003             buf.append('"').append(parentName).append('.').append(attribute.getColumnName()).append("\", \"");
1004           }
1005           buf.append(join.getName()).append(".id\", ")
1006              .append(joinClass).append(".class, \"").append(join.getName()).append("\",\n")
1007              .append(inset).append("    (").append(leftClass).append(" ").append(alias).append(", ")
1008              .append(joinClass).append(" ").append(joinAlias).append(") -> {\n")
1009              .append(inset).append("      ((").append(poImpl).append(") ")
1010              .append(alias).append('.');
1011           if (relation.getEntity().isEmbedded()) {
1012             buf.append(join.getArgument().getEmbeddingGetterPrefix());
1013             StringTokenizer stok = new StringTokenizer(relation.getPathName(), ".");
1014             while(stok.hasMoreTokens()) {
1015               buf.append("get").append(stok.nextToken()).append("().");
1016             }
1017           }
1018           buf.append("getPersistenceDelegate()).")
1019              .append(relation.getSetterName());
1020           if (relation.isSerialized()) {
1021             buf.append("Blunt");
1022           }
1023           buf.append("(").append(joinAlias).append(");\n");
1024         }
1025         buf.append(inset).append("    }\n")
1026            .append(inset).append("  )\n");
1027       }
1028 
1029       List<JoinPath> subPaths = path.getPaths();
1030       if (!subPaths.isEmpty()) {
1031         // all sub paths start with the same entity
1032         Relation subRelation = subPaths.get(0).getElements().getFirst().getWurbletRelation().getRelation();
1033         Entity subEntity = subRelation.getEntity();
1034         createJoinsImpl(buf, subEntity.isEmbedded() ? lastNonEmbeddedEntity : subEntity, inset + "  ", subPaths, subEntity.getName(), lastJoinName);
1035       }
1036       buf.append(inset).append(")\n");
1037     }
1038   }
1039 
1040 
1041   private String createOptionalWhereForJoin(Relation relation, String parentName, Join join) throws WurbelException {
1042     StringBuilder buf = new StringBuilder();
1043     List<MethodArgument> methodArgs = relation.getMethodArgs();
1044     if (methodArgs.size() > 1) {
1045       for (MethodArgument methodArg: methodArgs.subList(1, methodArgs.size())) {
1046         Attribute attr = methodArg.getForeignAttribute();
1047         buf.append("\" AND ").append(join.getName()).append('.')
1048            .append(attr.getColumnName())
1049            .append('=');
1050         if (methodArg.isValue()) {
1051           buf.append('?');
1052           DataType<?> effectiveDataType = getEffectiveDataType(attr);
1053           if (effectiveDataType.isColumnCountBackendSpecific()) {
1054             throw new WurbelException("backend-specific datatype " + effectiveDataType + " cannot be used in joins");
1055           }
1056           for (int columnIndex = 1; columnIndex < effectiveDataType.getColumnCount(null); columnIndex++) {   // for more than one column
1057             buf.append(" AND ").append(join.getName()).append('.').append(getColumnName(attr, columnIndex)).append("=?");
1058           }
1059           buf.append('"');
1060         }
1061         else {    // join by ID
1062           attr = methodArg.getAttribute();
1063           if (parentName == null) {
1064             buf.append("\" + getColumnName(").append(getColumnNameConstant(attr, 0)).append(")");
1065           }
1066           else {
1067             buf.append('"').append(parentName).append('.').append(attr.getColumnName()).append("\"");
1068           }
1069         }
1070       }
1071     }
1072 
1073     WurbletArgumentParser joinFilterParser = join.getWurbletRelation().getParser();
1074     if (joinFilterParser != null && join.getName() != null) {
1075       if (buf.isEmpty()) {
1076         buf.append('"');
1077       }
1078       else if (buf.charAt(buf.length() - 1) == '"') {
1079         buf.deleteCharAt(buf.length() - 1);
1080       }
1081       else {
1082         buf.append(" + \"");
1083       }
1084       buf.append(new JoinClauseGenerator(this, join).generate()).append('"');
1085     }
1086 
1087     return buf.toString();
1088   }
1089 
1090 
1091   /**
1092    * Creates the java code to set a parameter in a PreparedStatementWrapper.<br>
1093    * Works for single- and multi-column data types.
1094    *
1095    * @param arg the wurblet argument
1096    * @return the Java code
1097    * @throws WurbelException if malformed or illegal wurblet argument
1098    * @throws ModelException if getting the JDBC Java code failed
1099    */
1100   public String createWhereSetPars(WurbletArgument arg) throws WurbelException, ModelException {
1101     Attribute attr = arg.getAttribute();
1102     int columnIndex = 0;
1103     if (arg.isArray()) {
1104       String javaType;
1105       if (attr.isConvertible()) {
1106         javaType = attr.getInnerTypeName();
1107         columnIndex = -1;
1108       }
1109       else {
1110         javaType = attr.getDataType().toNonPrimitive().getJavaType();
1111         if (arg.getColumnIndex() != null) {
1112           columnIndex = arg.getColumnIndex();
1113         }
1114       }
1115       return "st.setArray(ndx++, " + javaType + ".class, " + columnIndex + ", " +
1116              attr.toMethodArgument(arg.getJdbcValue()) + ", Backend.SQL_ARRAY_" +
1117              arg.getArrayOperator().replace(' ', '_') + ")";
1118     }
1119     if (arg.getColumnIndex() == null) {
1120       return createWhereSetPars(attr, attr.toMethodArgument(arg.getJdbcValue()));
1121     }
1122     columnIndex = arg.getColumnIndex();
1123     StringBuilder buf = new StringBuilder();
1124     if (arg.getDataType().isPredefined()) {   // predefined multi-column types like BMoney, OffsetDateTime, etc...
1125       String jdbcValue = arg.getJdbcValue();
1126       buf.append("st.set(SqlType.").append(arg.getDataType().getSqlType(null, columnIndex)).append(", ndx++, ");
1127       if (arg.isMethodArgument()) {
1128         buf.append(arg.getDataType().getColumnGetter(columnIndex, jdbcValue));
1129       }
1130       else {
1131         buf.append(jdbcValue);
1132       }
1133       buf.append(")");
1134     }
1135     else {
1136       buf.append("ndx += st.set(");
1137       if (!attr.getEntity().equals(getEntity())) {
1138         buf.append(deriveClassNameForEntity(attr.getEntity())).append('.');
1139       }
1140       buf.append(arg.getDataType().getDataTypeConstant())
1141          .append(", ndx, ")
1142          .append(arg.getJdbcValue())
1143          .append(", ").append(columnIndex)
1144          .append(", ").append(attr.getOptions().isMapNull())
1145          .append(", ").append(attr.getSize()).append(")");
1146     }
1147     return buf.toString();
1148   }
1149 
1150 
1151   /**
1152    * Creates the java code to set a single-column parameter in a PreparedStatementWrapper.
1153    *
1154    * @param attr the model attribute
1155    * @param argument the argument string
1156    * @return the created Java code
1157    * @throws WurbelException if failed
1158    */
1159   public String createWhereSetPars(Attribute attr, String argument) throws WurbelException {
1160     StringBuilder buf = new StringBuilder();
1161     DataType<?> dataType = getEffectiveDataType(attr);
1162     if (dataType.isPredefined()) {
1163       buf.append("st.")
1164          .append(createJdbcSetterName(dataType))
1165          .append("(ndx++, ")
1166          .append(getJdbcCode(attr, argument));
1167       if (attr.getOptions().isMapNull()) {
1168         buf.append(", true)");
1169       }
1170       else {
1171         buf.append(")");
1172       }
1173       // for multi-column types, such as BMoney (not application-specific types)
1174       if (dataType.isColumnCountBackendSpecific()) {
1175         throw new WurbelException("backend-specific datatype " + dataType + " cannot be used in where-clauses");
1176       }
1177       int diff = dataType.getColumnCount(null) - 1;
1178       if (diff == 1) {
1179         buf.append(";\n    ndx++");
1180       }
1181       else if (diff > 1) {
1182         buf.append(";\n    ndx += ").append(diff);
1183       }
1184     }
1185     else {
1186       buf.append("ndx += st.set(");
1187       if (!attr.getEntity().equals(getEntity())) {
1188         buf.append(deriveClassNameForEntity(attr.getEntity())).append('.');
1189       }
1190       buf.append(dataType.getDataTypeConstant())
1191          .append(", ndx, ")
1192          .append(getJdbcCode(attr, argument))
1193          .append(", ").append(attr.getOptions().isMapNull())
1194          .append(", ").append(attr.getSize()).append(")");
1195     }
1196     return buf.toString();
1197   }
1198 
1199 
1200   /**
1201    * Creates optional JDBC-set parameters code for joins with extra arguments.
1202    *
1203    * @return the optional code, empty if none
1204    * @throws WurbelException if failed
1205    */
1206   public String createJoinSetPars() throws WurbelException, ModelException {
1207     StringBuilder buf = new StringBuilder();
1208     for (JoinPath path: getJoinPaths()) {
1209       createJoinSetPars(buf, path);
1210     }
1211     return buf.toString();
1212   }
1213 
1214   private void createJoinSetPars(StringBuilder buf, JoinPath path) throws WurbelException, ModelException {
1215     if (!path.getElements().isEmpty()) {
1216       Join join = path.getElements().get(0);    // normalized, i.e., only one (see above)
1217       Relation relation = join.getWurbletRelation().getRelation();
1218       List<MethodArgument> methodArgs = relation.getMethodArgs();
1219       if (methodArgs.size() > 1) {
1220         for (MethodArgument methodArg: methodArgs.subList(1, methodArgs.size())) {
1221           if (methodArg.isValue()) {
1222             Attribute attr = methodArg.getForeignAttribute();
1223             buf.append("    ").append(createWhereSetPars(attr, attr.toMethodArgument(methodArg.getValue()))).append(";\n");
1224           }
1225         }
1226       }
1227       for (JoinPath subPath: path.getPaths()) {
1228         createJoinSetPars(buf, subPath);
1229       }
1230     }
1231   }
1232 
1233 
1234   /**
1235    * Creates the name of a statement id.<br>
1236    * Because statement ids are final, the name is in uppercase.
1237    *
1238    * @return the id string
1239    * @throws WurbelException if guard name could not be determined
1240    */
1241   public String createStatementId() throws WurbelException {
1242     StringBuilder buf = new StringBuilder();
1243     String id = getGuardName();
1244     for (int i=0; i < id.length(); i++) {
1245       char c = id.charAt(i);
1246       if (Character.isUpperCase(c) && !buf.isEmpty()) {
1247         buf.append('_');
1248       }
1249       buf.append(Character.toUpperCase(c));
1250     }
1251     buf.append("_STMT");
1252     return buf.toString();
1253   }
1254 
1255 
1256   /**
1257    * Builds the string of method parameters.
1258    *
1259    * @param limit optional limit rows
1260    * @param offset optional skip rows
1261    * @return the method parameters
1262    * @throws WurbelException if failed
1263    */
1264   public String buildMethodParameters(boolean limit, boolean offset) throws WurbelException {
1265 
1266     StringBuilder params = new StringBuilder();
1267 
1268     if (limit) {
1269       appendCommaSeparated(params, "int limit");
1270     }
1271     if (offset) {
1272       appendCommaSeparated(params, "int offset");
1273     }
1274 
1275     Set<String> argSet = new HashSet<>();     // each arg only once
1276     for (WurbletArgument key: getMethodArguments())  {
1277       if (key.isMethodArgument()) {
1278         String arg = key.getMethodArgumentName();
1279         if (argSet.add(arg)) {
1280           Attribute attr = key.getAttribute();
1281           try {
1282             String javaType;
1283             if (key.isArray()) {
1284               String elementType;
1285               if (attr.isConvertible()) {
1286                 elementType = attr.getApplicationTypeName();
1287               }
1288               else {
1289                 elementType = attr.getDataType().toNonPrimitive().getJavaType();
1290               }
1291               javaType = "Collection<" + elementType + ">";
1292             }
1293             else {
1294               javaType = attr.getJavaType();
1295             }
1296             appendCommaSeparated(params, javaType);
1297           }
1298           catch (ModelException me) {
1299             throw new WurbelException("cannot determine java type for key " + key, me);
1300           }
1301           params.append(' ');
1302           params.append(arg);
1303         }
1304       }
1305       // the boolean determining whether an optional condition is part of the query
1306       if (key.isOptional() && argSet.add(key.getOptionalName())) {
1307         appendCommaSeparated(params, "boolean");
1308         params.append(' ');
1309         params.append(key.getOptionalName());
1310       }
1311     }
1312 
1313     return params.toString();
1314   }
1315 
1316 
1317   /**
1318    * Builds the string of method parameters.
1319    *
1320    * @return the method parameters
1321    * @throws WurbelException if failed
1322    */
1323   public String buildMethodParameters() throws WurbelException {
1324     return buildMethodParameters(false, false);
1325   }
1326 
1327 
1328   /**
1329    * Builds the string of invocation parameters.
1330    *
1331    * @param limit optional limit rows
1332    * @param offset optional skip rows
1333    * @return the invocation parameters
1334    * @throws WurbelException if failed
1335    */
1336   public String buildInvocationParameters(boolean limit, boolean offset) throws WurbelException {
1337 
1338     StringBuilder params = new StringBuilder();
1339 
1340     if (limit) {
1341       appendCommaSeparated(params, "limit");
1342     }
1343     if (offset) {
1344       appendCommaSeparated(params, "offset");
1345     }
1346 
1347     Set<String> argSet = new HashSet<>();     // each arg only once
1348     for (WurbletArgument key: getMethodArguments())  {
1349       if (key.isMethodArgument()) {
1350         String arg = key.getMethodArgumentName();
1351         if (argSet.add(arg)) {
1352           appendCommaSeparated(params, arg);
1353         }
1354       }
1355       if (key.isOptional() && argSet.add(key.getOptionalName())) {
1356         appendCommaSeparated(params, key.getOptionalName());
1357       }
1358     }
1359 
1360     return params.toString();
1361   }
1362 
1363 
1364   /**
1365    * Determines whether one of the wurblet arguments refers to a value returned by a PDO's method.
1366    *
1367    * @return true if at least one argument is something like "getBlah()"
1368    */
1369   public boolean isPdoProvidingArguments() {
1370     for (WurbletArgument key: getMethodArguments())  {
1371       if (!key.isMethodArgument() && !key.isValueLiterally()) {
1372         String arg = key.getValue();
1373         if (arg != null && arg.endsWith("()")) {
1374           return true;
1375         }
1376       }
1377     }
1378     return false;
1379   }
1380 
1381 
1382   /**
1383    * Builds the string of invocation parameters.
1384    *
1385    * @return the invocation parameters
1386    * @throws WurbelException if failed
1387    */
1388   public String buildInvocationParameters() throws WurbelException {
1389     return buildInvocationParameters(false, false);
1390   }
1391 
1392 
1393   /**
1394    * Adds a string to a comma-separated list.
1395    *
1396    * @param str the string
1397    * @param appendStr the string to append
1398    * @return the new string
1399    */
1400   public String acs(String str, String appendStr) {
1401     StringBuilder builder = new StringBuilder(str);
1402     appendCommaSeparated(builder, appendStr);
1403     return builder.toString();
1404   }
1405 
1406   /**
1407    * Prepends a string to a comma-separated list.
1408    *
1409    * @param str the string builder
1410    * @param prependStr the string to prepend
1411    * @return the new string
1412    */
1413   public String pcs(String str, String prependStr) {
1414     StringBuilder builder = new StringBuilder(str);
1415     prependCommaSeparated(builder, prependStr);
1416     return builder.toString();
1417   }
1418 
1419 
1420 
1421   /**
1422    * Utility method to generate the argument list.
1423    *
1424    * @param str the argument string
1425    * @return comma + str if str not empty or null, the empty string otherwise
1426    */
1427   public String aas(String str) {
1428     if (str != null && !str.isEmpty()) {
1429       return ", " + str;
1430     }
1431     else  {
1432       return "";
1433     }
1434   }
1435 
1436 
1437   /**
1438    * Utility method to always get a non-null string.
1439    *
1440    * @param str the source string
1441    * @return the string or the empty string if str is null
1442    */
1443   public String as(String str) {
1444     return str == null ? "" : str;
1445   }
1446 
1447 
1448   /**
1449    * Creates the "setXXXX" method name for PreparedStatementWrapper.
1450    *
1451    * @param dataType the datatype
1452    * @return the method name
1453    */
1454   public String createJdbcSetterName(DataType<?> dataType) {
1455     StringBuilder buf = new StringBuilder("set");
1456     if (dataType.isPrimitive()) {
1457       buf.append(StringHelper.firstToUpper(dataType.getJavaType()));
1458     }
1459     else  {
1460       String variant = dataType.getVariant();
1461       if (variant.isEmpty()) {
1462         buf.append(dataType.getMethodTypeName());
1463       }
1464       else {
1465         buf.append(variant);
1466       }
1467     }
1468     return buf.toString();
1469   }
1470 
1471   /**
1472    * Creates the "getXXXX" method name for ResultSetWrapper.
1473    *
1474    * @param attribute the model attribute
1475    * @return the method name
1476    * @throws WurbelException if the type is misconfigured
1477    */
1478   public String createJdbcGetterName(Attribute attribute) throws WurbelException {
1479     StringBuilder buf = new StringBuilder("get");
1480     DataType<?> dataType = getEffectiveDataType(attribute);
1481     if (dataType.isPrimitive()) {
1482       buf.append(StringHelper.firstToUpper(dataType.getJavaType()));
1483     }
1484     else {
1485       switch (dataType.getJavaType()) {
1486         case "Boolean":
1487         case "Byte":
1488         case "Short":
1489         case "Long":
1490         case "Float":
1491         case "Double":
1492           buf.append('A');   // ABoolean, AByte, etc...
1493           break;
1494       }
1495       String variant = dataType.getVariant();
1496       if (variant.isEmpty()) {
1497         buf.append(dataType.getMethodTypeName());
1498       }
1499       else {
1500         buf.append(variant);
1501       }
1502     }
1503     return buf.toString();
1504   }
1505 
1506 
1507   /**
1508    * Creates java code applicable to the application model from JDBC variable access code.<br>
1509    * Applies .toInternal() if the attribute is an application-specific type.
1510    * Otherwise, jdbcCode is returned unchanged.
1511    *
1512    * @param attribute the attribute
1513    * @param jdbcCode the jdbc code
1514    * @return the code the model code
1515    * @throws WurbelException if some model error
1516    */
1517   public String getModelCode(Attribute attribute, String jdbcCode) throws WurbelException {
1518     if (attribute.isConvertible()) {
1519       try {
1520         return attribute.getJavaType() + ".toInternal(" + jdbcCode + ")";
1521       }
1522       catch (ModelException me) {
1523         throw new WurbelException("cannot determine java model-side code for " + attribute + ": " + jdbcCode, me);
1524       }
1525     }
1526     else {
1527       return jdbcCode;
1528     }
1529   }
1530 
1531   /**
1532    * Creates java code applicable to the JDBC layer from model variable access code.<br>
1533    * Applies .toExternal() if the attribute is an application-specific type.
1534    * Otherwise, modelCode is returned unchanged.
1535    *
1536    * @param attribute the attribute
1537    * @param modelCode the jdbc code
1538    * @return the code the model code
1539    * @throws WurbelException if some model error
1540    */
1541   public String getJdbcCode(Attribute attribute, String modelCode) throws WurbelException {
1542     String jdbcCode = modelCode;
1543     if (attribute.isConvertible()) {
1544       try {
1545         String applicationType = attribute.getApplicationTypeName();
1546         if (isConstant(modelCode)) {
1547           // (Enum) constant, never null
1548           jdbcCode = modelCode + ".toExternal()";
1549         }
1550         else {
1551           if (attribute.getInnerDataType().isPrimitive()) {
1552             jdbcCode = modelCode + " == null ? " + applicationType + ".getDefault().toExternal() : " +
1553                        modelCode + ".toExternal()";
1554           }
1555           else {
1556             // is nullable
1557             jdbcCode = modelCode + " == null ? null : " + modelCode + ".toExternal()";
1558           }
1559         }
1560       }
1561       catch (ModelException me) {
1562         throw new WurbelException("cannot determine java jdbc-side code for " + attribute + ": " + modelCode, me);
1563       }
1564     }
1565     return jdbcCode;
1566   }
1567 
1568 
1569   /**
1570    * Returns whether the given java code refers to a constant.<br>
1571    * Since the wurblet cannot determine the java type of the code, the decision is made
1572    * according to the Java code conventions: {@code [<class>.]<VALUE_IN_UPPERCASE>}
1573    * is a constant and assumed to be not null.
1574    *
1575    * @param code the java code
1576    * @return true if constant
1577    */
1578   public boolean isConstant(String code) {
1579     return code != null && CONSTANT_PATTERN.matcher(code).matches();
1580   }
1581 
1582 
1583   /**
1584    * Returns whether the relation is transient.
1585    *
1586    * @param relation the relation
1587    * @return true if transient modifier required
1588    */
1589   public boolean isRelationTransient(Relation relation) {
1590     return relation.getSelectionType() == SelectionType.LAZY && !relation.isComposite() && !relation.isSerialized();
1591   }
1592 
1593 
1594   /**
1595    * Creates the argument string for the {@code select} or {@code delete} statement of a relation.
1596    *
1597    * @param relation the relation.
1598    * @return the arg string
1599    */
1600   public String createRelationArgString(Relation relation) {
1601     StringBuilder buf = new StringBuilder();
1602     buf.append('(');
1603     List<MethodArgument> args = relation.getMethodArgs();
1604     boolean first = true;
1605     for (MethodArgument arg: args) {
1606       if (first) {
1607         first = false;
1608       }
1609       else  {
1610         buf.append(", ");
1611       }
1612       buf.append(arg.getMethodArgument());
1613     }
1614     buf.append(')');
1615     return buf.toString();
1616   }
1617 
1618 
1619   /**
1620    * Creates the wurblet argument string for {@code select} or {@code delete} statement of a relation.
1621    *
1622    * @param relation the relation.
1623    * @return the arg string
1624    */
1625   public String createRelationWurbletArgString(Relation relation) {
1626     StringBuilder buf = new StringBuilder();
1627     List<MethodArgument> args = relation.getMethodArgs();
1628     for (MethodArgument arg: args) {
1629       if (!buf.isEmpty()) {
1630         buf.append(' ');
1631       }
1632       buf.append(arg.getForeignAttribute().getName());
1633     }
1634     return buf.toString();
1635   }
1636 
1637 
1638   /**
1639    * Creates the java code to select a relation.
1640    *
1641    * @param relation the relation
1642    * @return the Java code
1643    */
1644   public String createRelationSelectCode(Relation relation) {
1645     StringBuilder text = new StringBuilder();
1646     if (relation.getForeignEntity().isAbstract() &&
1647         relation.getRelationType() == RelationType.OBJECT &&
1648         relation.getMethodName() == null) {
1649       /*
1650        * we need a cast for object relations of type T and methods "select" or "selectCached" (i.e., from the framework).
1651        * Ex.:
1652        *
1653        * OrgUnit<?> ou = on(OrgUnit.class).select(id);    ERROR because on(OrgUnit.class) is of type OrgUnit and not OrgUnit<?>
1654        *
1655        * but:
1656        *
1657        * OrgUnit<?> ou = on(OrgUnit.class);  OK!
1658        * ou = ou.select(id);                 OK!
1659        */
1660       text.append("(").append(relation.getClassName()).append("<?>) ");
1661     }
1662     text.append("on(").append(relation.getClassName()).append(".class).")
1663         .append(createRelationSelectMethodName(relation))
1664         .append(createRelationArgString(relation));
1665     return text.toString();
1666   }
1667 
1668 
1669   /**
1670    * Creates the java code to update the reference of a relation.
1671    *
1672    * @param relation the relation
1673    * @param pdo the PDO name
1674    * @param blunt use the blunt setter method, if provided
1675    * @return the Java code
1676    */
1677   public String createRelationUpdateReferenceCode(Relation relation, String pdo, boolean blunt) {
1678     Relation foreignRel = relation.getForeignRelation();
1679     if (blunt &&
1680         (foreignRel == null ||
1681          foreignRel.getRelationType() != RelationType.OBJECT || !foreignRel.isSerialized() ||
1682          (foreignRel.getSelectionType() != SelectionType.LAZY && foreignRel.getSelectionType() != SelectionType.EAGER))) {
1683       blunt = false;
1684     }
1685 
1686     if (!blunt && relation.getLinkMethodName() != null) {
1687       return relation.getLinkMethodName() + (relation.getLinkMethodIndex() != null ?
1688               ("(" + pdo + ", ndx++)") : ("(" + pdo + ")"));
1689     }
1690     else {
1691       if (foreignRel != null) {
1692         return foreignRel.getSetterName() + (blunt ? "Blunt" : "") + "(" + pdo + ")";
1693       }
1694       // foreignRel == null -> blunt = false (see above)
1695       return "set" + relation.getForeignEntity() + "(" + pdo + ")";
1696     }
1697   }
1698 
1699   /**
1700    * Creates the java code to update the reference of a relation.<br>
1701    * The pdo name is fixed {@code "me()"}.
1702    *
1703    * @param relation the relation
1704    * @return the Java code
1705    */
1706   public String createRelationUpdateReferenceCode(Relation relation) {
1707     return createRelationUpdateReferenceCode(relation, "me()", false);
1708   }
1709 
1710   /**
1711    * Creates the set-first-arg method name (useful only in object relations).
1712    *
1713    * @param relation the relation
1714    * @return the set-method name
1715    * @throws ModelException if unexpected method args
1716    */
1717   public String createRelationSetFirstArgMethodName(Relation relation) throws ModelException {
1718     if (relation.getMethodArgs().size() > 1) {
1719       throw new ModelException("more than one method argument for relation " + relation, relation);
1720     }
1721     return "set" + StringHelper.firstToUpper(getFirstMethodAttribute(relation).getName());
1722   }
1723 
1724 
1725   /**
1726    * Gets the method attribute for a relation.
1727    *
1728    * @param relation the relation
1729    * @return the attribute
1730    * @throws ModelException if no method args found or missing attribute
1731    */
1732   public Attribute getFirstMethodAttribute(Relation relation) throws ModelException {
1733     List<MethodArgument> methodArgs = relation.getMethodArgs();
1734     if (methodArgs.isEmpty()) {
1735       throw new ModelException("no method arguments for relation " + relation, relation);
1736     }
1737     Attribute attribute = methodArgs.get(0).getAttribute();
1738     if (attribute == null) {
1739       throw new ModelException("missing attribute for relation " + relation, relation);
1740     }
1741     return attribute;
1742   }
1743 
1744 
1745   /**
1746    * Creates the java code to delete a relation.
1747    *
1748    * @param relation the relation
1749    * @return the Java code
1750    */
1751   public String createRelationDeleteCode(Relation relation) {
1752     return "on(" + relation.getClassName() + ".class)." +
1753            createListRelationDeleteMethodName(relation) +
1754            createRelationArgString(relation);
1755   }
1756 
1757 
1758   /**
1759    * Creates the java code to set the link of a relation.
1760    *
1761    * @param relation the relation
1762    * @return the Java code
1763    */
1764   public String createRelationLinkCode(Relation relation) {
1765     if (relation.getLinkMethodName() != null) {
1766       return relation.getLinkMethodName() + (relation.getLinkMethodIndex() != null ? "(me(), ndx++)" : "(me())");
1767     }
1768     String text = "set";
1769     if (relation.getMethodName() != null) {
1770       text += relation.getMethodName();
1771     }
1772     else {
1773       text += relation.getEntity().getName() + "Id";
1774     }
1775     return text + createRelationArgString(relation);
1776   }
1777 
1778 
1779   /**
1780    * The TT persistence layer provides an optimized {@code select} for eager relations via LeftJoins.
1781    * <p>
1782    * The joins are applied to the generic selects, i.e. to every read of the entity. Each relation
1783    * cascades as deep as its {@link Relation#getEagerDepth()} allows, the default 1 joining the
1784    * related entity only. Deeper levels follow the eager relations of the joined entity, which is
1785    * what turns {@code A -eager-&gt; B -eager-&gt; C} into a single statement.
1786    * <p>
1787    * The whole optimization is abandoned (an empty list is returned, so the relations are loaded by
1788    * follow-up selects) if a subclass of the entity declares an eager relation, because the joins of
1789    * a polymorphic select cannot depend on the row's class.
1790    *
1791    * @return the eager join tree, empty list if no optimization possible or no eager relations at all
1792    */
1793   public List<EagerJoin> getEagerJoinTree() {
1794     // the joins of a select on the base class cannot depend on the class of the row
1795     for (Relation rel: getEntity().getSubEntityRelations()) {
1796       if (rel.getSelectionType() == SelectionType.EAGER) {
1797         return new ArrayList<>();
1798       }
1799     }
1800     return createEagerJoinTree(getEntity().getRelations(), "e", 0);
1801   }
1802 
1803   /**
1804    * Creates the eager joins for one level of the tree.
1805    *
1806    * @param relations the relations to inspect
1807    * @param aliasPrefix the alias prefix of this level
1808    * @param remainingDepth the number of hops still allowed, 0 to take it from the relation itself
1809    * @return the joins, empty if none
1810    */
1811   private List<EagerJoin> createEagerJoinTree(List<Relation> relations, String aliasPrefix, int remainingDepth) {
1812     List<EagerJoin> eagerJoins = new ArrayList<>();
1813     int joinCount = 0;
1814     for (Relation relation: relations) {
1815       if (relation.getSelectionType() == SelectionType.EAGER) {
1816         // a root relation brings its own budget, a cascaded one spends the budget of its root
1817         int depth = remainingDepth == 0 ? relation.getEagerDepth() : remainingDepth;
1818         String alias = aliasPrefix + "_" + ++joinCount;
1819         EagerJoin eagerJoin = new EagerJoin(relation, alias);
1820         if (depth > 1) {
1821           eagerJoin.getJoins().addAll(
1822                   createEagerJoinTree(relation.getForeignEntity().getRelationsIncludingInherited(), alias, depth - 1));
1823         }
1824         eagerJoins.add(eagerJoin);
1825       }
1826     }
1827     return eagerJoins;
1828   }
1829 
1830   /**
1831    * Creates the java code for the eager joins of the class variables.
1832    * <p>
1833    * The result is the whole trailing argument of {@code PersistentObjectClassVariables.create(...)},
1834    * i.e. either {@code null);} or the complete {@code List.of(...));} block.
1835    * <p>
1836    * This is deliberately kept apart from {@link #createJoins()}, which emits the same kind of join
1837    * tree for the load joins of the {@code PdoSelect} wurblets: the class variables are a <em>static</em>
1838    * initializer, so the joined columns must be plain string literals, whereas {@code createJoins()}
1839    * runs in an instance context and can refer to {@code getColumnName(...)}.
1840    *
1841    * @return the generated Java code
1842    * @throws WurbelException if failed
1843    * @throws ModelException if the model does not allow the eager joins
1844    */
1845   public String createEagerJoins() throws WurbelException, ModelException {
1846     List<EagerJoin> eagerJoins = getEagerJoinTree();
1847     if (eagerJoins.isEmpty()) {
1848       return "                    null);\n";
1849     }
1850     StringBuilder buf = new StringBuilder("                    List.of(\n");
1851     buf.append(String.join(",\n",
1852                            createEagerJoinTexts(getEntity(), getPdoClassName(), getEntity().getTableAlias(),
1853                                                 "                      ", eagerJoins, true)));
1854     buf.append("\n                    )\n                );\n");
1855     return buf.toString();
1856   }
1857 
1858   /**
1859    * Creates the java code for one level of the eager join tree.
1860    *
1861    * @param entity the entity holding the relations
1862    * @param pdoName the PDO class name of that entity
1863    * @param parentName the table alias of that entity within the statement, also the lambda parameter name
1864    * @param inset the leading whitespace
1865    * @param eagerJoins the joins of this level
1866    * @param topLevel true if the joins belong to the entity the class variables are generated for
1867    * @return one text per join, without the separating comma
1868    * @throws WurbelException if failed
1869    * @throws ModelException if the model does not allow the eager joins
1870    */
1871   private List<String> createEagerJoinTexts(Entity entity, String pdoName, String parentName, String inset,
1872                                             List<EagerJoin> eagerJoins, boolean topLevel)
1873          throws WurbelException, ModelException {
1874 
1875     List<String> texts = new ArrayList<>();
1876     String poImpl = deriveClassNameForEntity(entity);
1877 
1878     for (EagerJoin eagerJoin: eagerJoins) {
1879       Relation relation = eagerJoin.getRelation();
1880       if (relation.getMethodArgs().size() > 1) {
1881         throw new ModelException("eager relations with 'args=...' are currently not supported", relation);
1882       }
1883       Entity foreignEntity = relation.getForeignEntity();
1884       String joinClass = foreignEntity.getName();
1885       String sqlAlias = eagerJoin.getAlias();
1886       // the lambda parameter names must be unique within the lambda: below the top level the table
1887       // aliases of the statement are used, because a relation may well point back to its own entity
1888       String joinName = topLevel ? foreignEntity.getTopSuperEntity().getTableAlias() : sqlAlias;
1889       // an abstract PDO cannot be inferred, and neither can the left side of a cascaded join
1890       boolean explicitTypes = foreignEntity.isAbstract() || !topLevel;
1891 
1892       String columnName;
1893       String joinedColumnName;
1894       if (relation.getRelationType() == RelationType.LIST) {
1895         columnName = parentName + ".id";
1896         joinedColumnName = sqlAlias + "." + relation.getForeignAttribute().getColumnName();
1897       }
1898       else {
1899         columnName = parentName + "." + relation.getAttribute().getColumnName();
1900         joinedColumnName = sqlAlias + ".id";
1901       }
1902 
1903       /*
1904        * A join carrying cascaded joins becomes the receiver of addJoin(), and the type of a
1905        * receiver is inferred on its own: the diamond would leave the owning PDO type unresolved,
1906        * so it has to be spelled out. A leaf join keeps the diamond, whose type arguments the
1907        * enclosing List.of() provides.
1908        */
1909       String diamond = eagerJoin.getJoins().isEmpty() ? "<>" : ("<" + pdoName + ", " + joinClass + ">");
1910 
1911       StringBuilder buf = new StringBuilder();
1912       buf.append(inset).append("new Join").append(diamond).append("(JoinType.LEFT, \"").append(columnName).append("\", \"")
1913          .append(joinedColumnName).append("\", ").append(joinClass).append(".class, \"").append(sqlAlias).append("\",\n")
1914          .append(inset).append("  (");
1915       if (explicitTypes) {
1916         buf.append(pdoName).append(' ');
1917       }
1918       buf.append(parentName).append(", ");
1919       if (explicitTypes) {
1920         buf.append(joinClass).append(' ');
1921       }
1922       buf.append(joinName).append(") -> {\n");
1923 
1924       String body = inset + "    ";
1925       if (foreignEntity.isAbstract()) {
1926         if (relation.getRelationType() == RelationType.LIST) {
1927           buf.append(body).append(poImpl).append(" po = (").append(poImpl).append(") ")
1928              .append(parentName).append(".getPersistenceDelegate();\n")
1929              .append(body).append("po.").append(relation.getGetterName()).append("Blunt().add(")
1930              .append(joinName).append(");\n");
1931         }
1932         else {
1933           buf.append(body).append(parentName).append('.').append(relation.getSetterName())
1934              .append('(').append(joinName).append(");\n");
1935         }
1936       }
1937       else if (relation.getRelationType() == RelationType.LIST) {
1938         if (relation.isReversed()) {
1939           buf.append(body).append("((").append(poImpl).append(") ").append(parentName)
1940              .append(".getPersistenceDelegate()).").append(relation.getSetterName())
1941              .append(relation.isSerialized() ? "Blunt" : "").append('(').append(joinName).append(");\n");
1942         }
1943         else {
1944           buf.append(body).append("((").append(poImpl).append(") ").append(parentName)
1945              .append(".getPersistenceDelegate()).").append(relation.getGetterName())
1946              .append("Blunt().addBlunt(").append(joinName).append(");\n");
1947           if (relation.getLinkMethodName() != null) {
1948             buf.append(body).append("((").append(deriveClassNameForEntity(foreignEntity)).append(") ")
1949                .append(joinName).append(".getPersistenceDelegate()).")
1950                .append(createRelationUpdateReferenceCode(relation, parentName, true)).append(";\n");
1951           }
1952         }
1953       }
1954       else {
1955         buf.append(body).append(parentName).append('.').append(relation.getSetterName())
1956            .append(relation.isSerialized() ? "Blunt" : "").append('(').append(joinName).append(");\n");
1957       }
1958 
1959       buf.append(inset).append("  }\n").append(inset).append(')');
1960 
1961       for (String subText: createEagerJoinTexts(foreignEntity, joinClass, sqlAlias, inset + "    ",
1962                                                 eagerJoin.getJoins(), false)) {
1963         buf.append('\n').append(inset).append("  .addJoin(\n")
1964            .append(subText).append('\n')
1965            .append(inset).append("  )");
1966       }
1967 
1968       texts.add(buf.toString());
1969     }
1970 
1971     return texts;
1972   }
1973 
1974 
1975   /**
1976    * Gets the eager joins as load join terms of the {@code PdoSelect} wurblets.
1977    * <p>
1978    * Used to add the eager relations to the generated relation finders so that they load the same
1979    * object graph as the generic selects do.
1980    * <p>
1981    * The tree differs from {@link #getEagerJoinTree()} in two ways, both of which predate the
1982    * cascade: the <em>inherited</em> eager relations are part of it, because a generated finder gets
1983    * no merging of the superclass joins the way
1984    * {@code PersistentObjectClassVariables#getEagerJoins()} does it at runtime; and the joins are
1985    * kept even when a subclass declares an eager relation.
1986    *
1987    * @return the paths, e.g. {@code "*invoice*lines"}, empty if no eager relations
1988    */
1989   public List<String> getEagerJoinPaths() {
1990     List<String> paths = new ArrayList<>();
1991     appendEagerJoinPaths(paths, "", createEagerJoinTree(getEntity().getRelationsIncludingInherited(), "e", 0));
1992     return paths;
1993   }
1994 
1995   /**
1996    * Collects the load join terms of an eager join tree.
1997    *
1998    * @param paths the collected paths
1999    * @param prefix the path leading to these joins
2000    * @param eagerJoins the joins of this level
2001    */
2002   private void appendEagerJoinPaths(List<String> paths, String prefix, List<EagerJoin> eagerJoins) {
2003     for (EagerJoin eagerJoin: eagerJoins) {
2004       String path = prefix + "*" + eagerJoin.getRelation().getName();
2005       if (eagerJoin.getJoins().isEmpty()) {
2006         paths.add(path);
2007       }
2008       else {
2009         // only the leaves are added: a load join term already implies all its parents
2010         appendEagerJoinPaths(paths, path, eagerJoin.getJoins());
2011       }
2012     }
2013   }
2014 
2015 }