View Javadoc
1   package org.synchronoss.cpo.core;
2   
3   /*-
4    * [[
5    * core
6    * ==
7    * Copyright (C) 2003 - 2026 Exaxis LLC, Synchronoss Technologies Inc
8    * ==
9    * This program is free software: you can redistribute it and/or modify
10   * it under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation, either version 3 of the
12   * License, or (at your option) any later version.
13   *
14   * This program is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17   * GNU General Lesser Public License for more details.
18   *
19   * You should have received a copy of the GNU General Lesser Public
20   * License along with this program.  If not, see
21   * <http://www.gnu.org/licenses/lgpl-3.0.html>.
22   * ]]
23   */
24  
25  import java.lang.reflect.InvocationTargetException;
26  import java.util.ArrayList;
27  import java.util.Collection;
28  import java.util.HashMap;
29  import java.util.List;
30  import java.util.Map.Entry;
31  import java.util.Set;
32  import org.slf4j.Logger;
33  import org.synchronoss.cpo.core.meta.MethodMapEntry;
34  import org.synchronoss.cpo.core.meta.MethodMapper;
35  import org.synchronoss.cpo.core.meta.domain.CpoArgument;
36  import org.synchronoss.cpo.core.meta.domain.CpoAttribute;
37  import org.synchronoss.cpo.core.meta.domain.CpoClass;
38  import org.synchronoss.cpo.core.meta.domain.CpoFunction;
39  import org.synchronoss.cpo.core.parser.BoundExpressionParser;
40  
41  /**
42   * CpoStatementFactory is the base class that encapsulates the creation of the native statement for
43   * the target datastore from the meta expression, where clauses, and order-by clauses.
44   *
45   * @author david berry
46   */
47  public abstract class CpoStatementFactory implements CpoReleasable {
48  
49    /** Version Id for this class. */
50    private static final long serialVersionUID = 1L;
51  
52    private Logger localLogger = null;
53  
54    private List<CpoReleasable> releasables = new ArrayList<>();
55    private static final String WHERE_MARKER = "__CPO_WHERE__";
56    private static final String ORDERBY_MARKER = "__CPO_ORDERBY__";
57  
58    private CpoStatementFactory() {
59      // hidden constructor
60    }
61  
62    /**
63     * Constructs a statement factory that logs to the given logger.
64     *
65     * @param localLogger the logger to use for statement-building diagnostics
66     */
67    public CpoStatementFactory(Logger localLogger) {
68      this.localLogger = localLogger;
69    }
70  
71    /**
72     * Gets the logger used for statement-building diagnostics.
73     *
74     * @return the logger used for statement-building diagnostics
75     */
76    public Logger getLocalLogger() {
77      return localLogger;
78    }
79  
80    /**
81     * Builds the final native statement text by combining the meta expression with dynamic where
82     * clauses, order-by clauses, and native expressions, substituting them at their markers when
83     * present or appending them otherwise.
84     *
85     * @param <T> the type of the JavaBean the wheres/orderBy are evaluated against
86     * @param cpoClass the meta class describing the bean being queried
87     * @param sql the base native expression from the meta data
88     * @param wheres dynamic where clauses to merge into the statement; may be {@code null}
89     * @param orderBy dynamic order-by clauses to merge into the statement; may be {@code null}
90     * @param nativeQueries native expressions to merge into the statement; may be {@code null}
91     * @param bindValues list that bind values contributed by the dynamic wheres are appended to, in
92     *     statement parameter order
93     * @return the fully assembled native statement text
94     * @throws CpoException if a where clause could not be translated to native syntax
95     */
96    protected <T> String buildSql(
97        CpoClass cpoClass,
98        String sql,
99        Collection<CpoWhere> wheres,
100       Collection<CpoOrderBy> orderBy,
101       Collection<CpoNativeFunction> nativeQueries,
102       List<BindAttribute> bindValues)
103       throws CpoException {
104     StringBuilder sqlText = new StringBuilder();
105 
106     sqlText.append(sql);
107 
108     if (wheres != null) {
109       for (CpoWhere where : wheres) {
110         BindableWhereBuilder<T> jwb = new BindableWhereBuilder<>(cpoClass);
111         BindableCpoWhere jcw = (BindableCpoWhere) where;
112 
113         // do the where stuff here when ready
114         jcw.acceptDFVisitor(jwb);
115 
116         if (sqlText.indexOf(jcw.getName()) == -1) {
117           sqlText.append(" ");
118           sqlText.append(jwb.getWhereClause());
119           bindValues.addAll(jwb.getBindValues());
120         } else {
121           sqlText = replaceMarker(sqlText, jcw.getName(), jwb, bindValues);
122         }
123       }
124     }
125 
126     // do the order by stuff now
127     if (orderBy != null) {
128       HashMap<String, StringBuilder> mapOrderBy = new HashMap<>();
129       for (CpoOrderBy ob : orderBy) {
130         StringBuilder sb = mapOrderBy.get(ob.getMarker());
131         if (sb == null) {
132           sb = new StringBuilder(" ORDER BY ");
133           mapOrderBy.put(ob.getMarker(), sb);
134         } else {
135           sb.append(",");
136         }
137         sb.append(ob.toString(cpoClass));
138       }
139 
140       Set<Entry<String, StringBuilder>> entries = mapOrderBy.entrySet();
141       for (Entry<String, StringBuilder> entry : entries) {
142         if (sqlText.indexOf(entry.getKey()) == -1) {
143           sqlText.append(entry.getValue().toString());
144         } else {
145           sqlText = replaceMarker(sqlText, entry.getKey(), entry.getValue().toString());
146         }
147       }
148     }
149 
150     if (nativeQueries != null) {
151       for (CpoNativeFunction cnq : nativeQueries) {
152         if (cnq.getMarker() == null || sqlText.indexOf(cnq.getMarker()) == -1) {
153           if (cnq.getExpression() != null && !cnq.getExpression().isEmpty()) {
154             sqlText.append(" ");
155             sqlText.append(cnq.getExpression());
156           }
157         } else {
158           sqlText = replaceMarker(sqlText, cnq.getMarker(), cnq.getExpression());
159         }
160       }
161     }
162 
163     // left for backwards compatibility
164     sqlText = replaceMarker(sqlText, WHERE_MARKER, "");
165     sqlText = replaceMarker(sqlText, ORDERBY_MARKER, "");
166 
167     return sqlText.toString();
168   }
169 
170   private StringBuilder replaceMarker(StringBuilder source, String marker, String replace) {
171     int attrOffset;
172     int fromIndex = 0;
173     int mLength = marker.length();
174     String replaceText = replace == null ? "" : replace;
175     int rLength = replaceText.length();
176 
177     // OUT.debug("starting string <"+source.toString()+">");
178     if (source != null && !source.isEmpty()) {
179       while ((attrOffset = source.indexOf(marker, fromIndex)) != -1) {
180         source.replace(attrOffset, attrOffset + mLength, replaceText);
181         fromIndex = attrOffset + rLength;
182       }
183     }
184     // OUT.debug("ending string <"+source.toString()+">");
185 
186     return source;
187   }
188 
189   private <T> StringBuilder replaceMarker(
190       StringBuilder source,
191       String marker,
192       BindableWhereBuilder<T> jwb,
193       List<BindAttribute> bindValues) {
194     int attrOffset;
195     int fromIndex = 0;
196     int mLength = marker.length();
197     String replace = jwb.getWhereClause();
198     int rLength = replace.length();
199     Collection<BindAttribute> jwbBindValues = jwb.getBindValues();
200 
201     // OUT.debug("starting string <"+source.toString()+">");
202     if (source != null && !source.isEmpty()) {
203       while ((attrOffset = source.indexOf(marker, fromIndex)) != -1) {
204         source.replace(attrOffset, attrOffset + mLength, replace);
205         fromIndex = attrOffset + rLength;
206         bindValues.addAll(
207             BoundExpressionParser.getBindMarkerIndexes(source.substring(0, attrOffset)).size(),
208             jwbBindValues);
209       }
210     }
211     // OUT.debug("ending string <"+source.toString()+">");
212 
213     return source;
214   }
215 
216   /**
217    * Adds a releasable object to this object. The release method on the releasable will be called
218    * when the PreparedStatement is executed.
219    *
220    * @param releasable the releasable to register; if {@code null} the call is a no-op
221    */
222   public void AddReleasable(CpoReleasable releasable) {
223     if (releasable != null) {
224       releasables.add(releasable);
225     }
226   }
227 
228   /**
229    * Called by the CPO framework. This method calls the <code>release</code> on all the
230    * CpoReleasable associated with this object
231    */
232   @Override
233   public void release() throws CpoException {
234     for (CpoReleasable releasable : releasables) {
235       try {
236         releasable.release();
237       } catch (CpoException ce) {
238         localLogger.error("Error Releasing Prepared Statement Transform Object", ce);
239         throw ce;
240       }
241     }
242   }
243 
244   /**
245    * Called by the CPO Framework. Binds all the attibutes from the class for the CPO meta parameters
246    * and the parameters from the dynamic where.
247    *
248    * @param function the meta function whose arguments describe the parameters to bind
249    * @param obj the bean instance to pull argument attribute values from
250    * @return the ordered list of bind values for {@code function}'s arguments
251    * @throws CpoException if one of {@code function}'s arguments is {@code null}
252    */
253   public List<BindAttribute> getBindValues(CpoFunction function, Object obj) throws CpoException {
254     List<BindAttribute> bindValues = new ArrayList<>();
255     List<CpoArgument> arguments = function.getArguments();
256     for (CpoArgument argument : arguments) {
257       if (argument == null) {
258         throw new CpoException("CpoArgument is null!");
259       }
260       bindValues.add(new BindAttribute(argument.getAttribute(), obj));
261     }
262     return bindValues;
263   }
264 
265   /**
266    * Called by the CPO Framework. Binds all the attibutes from the class for the CPO meta parameters
267    * and the parameters from the dynamic where.
268    *
269    * @param bindValues the bind values to apply to the underlying statement, in parameter order; if
270    *     {@code null} the call is a no-op
271    * @throws CpoException if a value could not be bound to the underlying statement
272    */
273   public void setBindValues(Collection<BindAttribute> bindValues) throws CpoException {
274 
275     if (bindValues != null) {
276       int index = getStartingIndex();
277 
278       // runs through the bind attributes and binds them to the prepared statement
279       // They must be in correct order.
280       for (BindAttribute bindAttr : bindValues) {
281         Object bindObject = bindAttr.bindObject();
282         CpoAttribute cpoAttribute = bindAttr.cpoAttribute();
283 
284         // check to see if we are getting a cpo value object or an object that
285         // can be put directly in the statement (String, BigDecimal, etc)
286         MethodMapEntry<?, ?> jsm =
287             ((MethodMapper<?>) getMethodMapper()).getDataMethodMapEntry(bindObject.getClass());
288 
289         if (jsm != null) {
290           try {
291             if (cpoAttribute == null) {
292               localLogger.debug("{}={}", bindAttr.name(), bindObject);
293             } else {
294               localLogger.debug("{}={}", cpoAttribute.getDataName(), bindObject);
295             }
296             jsm.getBsSetter().invoke(this.getBindableStatement(), index++, bindObject);
297           } catch (IllegalAccessException iae) {
298             String msg = "Error Accessing Prepared Statement Setter: ";
299             throw new CpoException(msg, iae);
300           } catch (InvocationTargetException ite) {
301             String msg = "Error Invoking Prepared Statement Setter: ";
302             throw new CpoException(msg, ite);
303           }
304         } else {
305           CpoData cpoData = getCpoData(cpoAttribute, index++);
306           cpoData.invokeSetter(bindObject);
307         }
308       }
309     }
310   }
311 
312   protected abstract MethodMapper getMethodMapper();
313 
314   protected abstract CpoData getCpoData(CpoAttribute cpoAttribute, int index);
315 
316   protected abstract Object getBindableStatement();
317 
318   protected abstract int getStartingIndex();
319 }