1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.cocoon.acting;
18
19 import java.io.BufferedInputStream;
20 import java.io.ByteArrayInputStream;
21 import java.io.File;
22 import java.io.FileInputStream;
23 import java.io.InputStream;
24 import java.math.BigDecimal;
25 import java.sql.Array;
26 import java.sql.Clob;
27 import java.sql.Date;
28 import java.sql.PreparedStatement;
29 import java.sql.ResultSet;
30 import java.sql.SQLException;
31 import java.sql.Time;
32 import java.sql.Timestamp;
33 import java.sql.Types;
34 import java.text.DateFormat;
35 import java.text.SimpleDateFormat;
36 import java.util.Collections;
37 import java.util.HashMap;
38 import java.util.Map;
39
40 import org.apache.avalon.excalibur.datasource.DataSourceComponent;
41 import org.apache.avalon.framework.activity.Disposable;
42 import org.apache.avalon.framework.configuration.Configuration;
43 import org.apache.avalon.framework.configuration.ConfigurationException;
44 import org.apache.avalon.framework.parameters.Parameters;
45 import org.apache.avalon.framework.service.ServiceException;
46 import org.apache.avalon.framework.service.ServiceManager;
47 import org.apache.avalon.framework.service.ServiceSelector;
48 import org.apache.cocoon.environment.Request;
49 import org.apache.cocoon.util.ImageProperties;
50 import org.apache.cocoon.util.ImageUtils;
51
52 /**
53 * Set up environment for configurable form handling data. It is
54 * important to note that all DatabaseActions use a common configuration
55 * format. This group of actions are unique in that they employ a
56 * terciary mapping. There is the Form parameter, the database column,
57 * and the type.
58 *
59 * Each configuration file must use the same format in order to be
60 * effective. The name of the root configuration element is irrelevant.
61 *
62 * <pre>
63 * <root>
64 * <connection>personnel<connection>
65 * <table>
66 * <keys>
67 * <key param="id" dbcol="id" type="int"/>
68 * </keys>
69 * <values>
70 * <value param="name" dbcol="name" type="string"/>
71 * <value param="department" dbcol="department_id" type="int"/>
72 * </values>
73 * </table>
74 * </root>
75 * </pre>
76 *
77 * The types recognized by this system are:
78 *
79 * <table>
80 * <tr>
81 * <th>Type</th>
82 * <th>Description</th>
83 * </tr>
84 * <tr>
85 * <td>ascii</td>
86 * <td>ASCII Input Stream, a CLOB input</td>
87 * </tr>
88 * <tr>
89 * <td>big-decimal</td>
90 * <td>a <code>java.math.BigDecimal</code> value</td>
91 * </tr>
92 * <tr>
93 * <td>binary</td>
94 * <td>Binary Input Stream, a BLOB input</td>
95 * </tr>
96 * <tr>
97 * <td>byte</td>
98 * <td>a Byte</td>
99 * </tr>
100 * <tr>
101 * <td>string</td>
102 * <td>a String</td>
103 * </tr>
104 * <tr>
105 * <td>date</td>
106 * <td>a Date</td>
107 * </tr>
108 * <tr>
109 * <td>double</td>
110 * <td>a Double</td>
111 * </tr>
112 * <tr>
113 * <td>float</td>
114 * <td>a Float</td>
115 * </tr>
116 * <tr>
117 * <td>int</td>
118 * <td>an Integer</td>
119 * </tr>
120 * <tr>
121 * <td>long</td>
122 * <td>a Long</td>
123 * </tr>
124 * <tr>
125 * <td>short</td>
126 * <td>a Short</td>
127 * </tr>
128 * <tr>
129 * <td>time</td>
130 * <td>a Time</td>
131 * </tr>
132 * <tr>
133 * <td>time-stamp</td>
134 * <td>a Timestamp</td>
135 * </tr>
136 * <tr>
137 * <td>now</td>
138 * <td>a Timestamp with the current day/time--the form value is ignored.</td>
139 * </tr>
140 * <tr>
141 * <td>image</td>
142 * <td>a binary image file, we cache the attribute information</td>
143 * </tr>
144 * <tr>
145 * <td>image-width</td>
146 * <td>
147 * the width attribute of the cached file attribute. NOTE:
148 * param attribute must equal the param for image with a
149 * "-width" suffix.
150 * </td>
151 * </tr>
152 * <tr>
153 * <td>image-height</td>
154 * <td>
155 * the width attribute of the cached file attribute NOTE:
156 * param attribute must equal the param for image with a
157 * "-height" suffix.
158 * </td>
159 * </tr>
160 * <tr>
161 * <td>image-size</td>
162 * <td>
163 * the size attribute of the cached file attribute NOTE:
164 * param attribute must equal the param for image with a
165 * "-size" suffix.
166 * </td>
167 * </tr>
168 * </table>
169 *
170 * @author <a href="mailto:bloritsch@apache.org">Berin Loritsch</a>
171 * @author <a href="mailto:balld@apache.org">Donald Ball</a>
172 * @version $Id: AbstractDatabaseAction.java 452425 2006-10-03 11:18:47Z vgritsenko $
173 */
174 public abstract class AbstractDatabaseAction extends AbstractComplementaryConfigurableAction
175 implements Disposable {
176
177 protected Map files = new HashMap();
178 protected static final Map typeConstants;
179 protected ServiceSelector dbselector;
180
181 static {
182 /*
183 * Initialize the map of type names to jdbc column types.
184 * Note that INTEGER, BLOB, and VARCHAR column types map to more than
185 * one type name.
186 */
187 Map constants = new HashMap();
188 constants.put("ascii", new Integer(Types.CLOB));
189 constants.put("big-decimal", new Integer(Types.BIGINT));
190 constants.put("binary", new Integer(Types.BLOB));
191 constants.put("byte", new Integer(Types.TINYINT));
192 constants.put("string", new Integer(Types.VARCHAR));
193 constants.put("date", new Integer(Types.DATE));
194 constants.put("double", new Integer(Types.DOUBLE));
195 constants.put("float", new Integer(Types.FLOAT));
196 constants.put("int", new Integer(Types.INTEGER));
197 constants.put("long", new Integer(Types.NUMERIC));
198 constants.put("short", new Integer(Types.SMALLINT));
199 constants.put("time", new Integer(Types.TIME));
200 constants.put("time-stamp", new Integer(Types.TIMESTAMP));
201 constants.put("now", new Integer(Types.LONGVARBINARY));
202 //constants.put("image", new Integer(Types.DISTINCT));
203 //constants.put("image-width", new Integer(Types.ARRAY));
204 //constants.put("image-height", new Integer(Types.BIT));
205 //constants.put("image-size", new Integer(Types.CHAR));
206 constants.put("image",new Integer(Types.BLOB));
207 constants.put("image-width",new Integer(Types.INTEGER));
208 constants.put("image-height",new Integer(Types.INTEGER));
209 constants.put("image-size",new Integer(Types.INTEGER));
210 constants.put("row-index",new Integer(Types.INTEGER));
211 constants.put("image-mime-type",new Integer(Types.VARCHAR));
212 constants.put("array", new Integer(Types.ARRAY));
213 constants.put("row", new Integer(Types.STRUCT));
214 constants.put("object", new Integer(Types.OTHER));
215 typeConstants = Collections.unmodifiableMap(constants);
216 }
217
218 /**
219 * Compose the Actions so that we can select our databases.
220 */
221 public void service(ServiceManager manager) throws ServiceException {
222 super.service(manager);
223 this.dbselector = (ServiceSelector) manager.lookup(DataSourceComponent.ROLE + "Selector");
224 }
225
226 /**
227 * Get the Datasource we need.
228 */
229 protected final DataSourceComponent getDataSource(Configuration conf) throws ServiceException {
230 Configuration dsn = conf.getChild("connection");
231 return (DataSourceComponent) this.dbselector.select(dsn.getValue(""));
232 }
233
234 /**
235 * Return whether a type is a Large Object (BLOB/CLOB).
236 */
237 protected final boolean isLargeObject (String type) {
238 if ("ascii".equals(type)) return true;
239 if ("binary".equals(type)) return true;
240 if ("image".equals(type)) return true;
241
242 return false;
243 }
244
245 /**
246 * Get the Statement column so that the results are mapped correctly.
247 */
248 protected Object getColumn(ResultSet set, Request request, Configuration entry)
249 throws Exception {
250 Integer type = (Integer) AbstractDatabaseAction.typeConstants.get(entry.getAttribute("type"));
251 String attribute = entry.getAttribute("param", "");
252 String dbcol = entry.getAttribute("dbcol", "");
253 Object value = null;
254
255 switch (type.intValue()) {
256 case Types.CLOB:
257 Clob dbClob = set.getClob(dbcol);
258 if (dbClob != null) {
259 int length = (int) dbClob.length();
260 InputStream is = new BufferedInputStream(dbClob.getAsciiStream());
261 try {
262 byte[] buffer = new byte[length];
263 length = is.read(buffer);
264 value = new String(buffer, 0, length);
265 } finally {
266 is.close();
267 }
268 }
269 break;
270 case Types.BIGINT:
271 value = set.getBigDecimal(dbcol);
272 break;
273 case Types.TINYINT:
274 value = new Byte(set.getByte(dbcol));
275 break;
276 case Types.VARCHAR:
277 value = set.getString(dbcol);
278 break;
279 case Types.DATE:
280 value = set.getDate(dbcol);
281 break;
282 case Types.DOUBLE:
283 value = new Double(set.getDouble(dbcol));
284 break;
285 case Types.FLOAT:
286 value = new Float(set.getFloat(dbcol));
287 break;
288 case Types.INTEGER:
289 value = new Integer(set.getInt(dbcol));
290 break;
291 case Types.NUMERIC:
292 value = new Long(set.getLong(dbcol));
293 break;
294 case Types.SMALLINT:
295 value = new Short(set.getShort(dbcol));
296 break;
297 case Types.TIME:
298 value = set.getTime(dbcol);
299 break;
300 case Types.TIMESTAMP:
301 value = set.getTimestamp(dbcol);
302 break;
303 case Types.ARRAY:
304 value = set.getArray(dbcol);
305 break;
306 case Types.BIT:
307 value = new Integer(set.getInt(dbcol));
308 break;
309 case Types.CHAR:
310 value = new Integer(set.getInt(dbcol));
311 break;
312 case Types.STRUCT:
313 value = set.getObject(dbcol);
314 break;
315 case Types.OTHER:
316 value = set.getObject(dbcol);
317 break;
318
319 default:
320 // The blob types have to be requested separately, via a Reader.
321 value = "";
322 break;
323 }
324
325 setRequestAttribute(request,attribute,value);
326
327 return value;
328 }
329
330 /**
331 * Set the Statement column so that the results are mapped correctly.
332 * The name of the parameter is retrieved from the configuration object.
333 *
334 * @param statement the prepared statement
335 * @param position the position of the column
336 * @param request the request
337 * @param entry the configuration object
338 */
339 protected void setColumn(PreparedStatement statement, int position, Request request, Configuration entry)
340 throws Exception {
341 setColumn(statement,position,request,entry,entry.getAttribute("param",""));
342 }
343
344 /**
345 * Set the Statement column so that the results are mapped correctly. The
346 * value of the column is retrieved from the request object. If the
347 * named parameter exists in the request object's parameters, that value
348 * is used. Otherwise if the named parameter exists in the request object's
349 * attributes, that value is used. Otherwise the request object is
350 * retrieved using Request.get(attribute), which is documented to be the
351 * same as Request.getAttribute(attribute), so something weird must be
352 * going on.
353 *
354 * @param statement the prepared statement
355 * @param position the position of the column
356 * @param request the request
357 * @param entry the configuration object
358 * @param param the name of the request parameter
359 */
360 protected void setColumn(PreparedStatement statement, int position, Request request, Configuration entry, String param)
361 throws Exception {
362 Object value = request.getParameter(param);
363 if (value == null) value = request.getAttribute(param);
364 if (value == null) value = request.get(param);
365 setColumn(statement,position,request,entry,param,value);
366 }
367
368 /**
369 * Set the Statement column so that the results are mapped correctly.
370 *
371 * @param statement the prepared statement
372 * @param position the position of the column
373 * @param request the request
374 * @param entry the configuration object
375 * @param param the name of the request parameter
376 * @param value the value of the column
377 */
378 protected void setColumn(PreparedStatement statement, int position, Request request, Configuration entry, String param, Object value) throws Exception {
379 setColumn(statement,position,request,entry,param,value,0);
380 }
381
382 /**
383 * Set the Statement column so that the results are mapped correctly.
384 *
385 * @param statement the prepared statement
386 * @param position the position of the column
387 * @param request the request
388 * @param entry the configuration object
389 * @param param the name of the request parameter
390 * @param value the value of the column
391 * @param rowIndex the index of the current row for manyrows inserts
392 */
393 protected void setColumn(PreparedStatement statement, int position, Request request, Configuration entry, String param, Object value, int rowIndex) throws Exception {
394 getLogger().debug("Setting column "+position+" named "+param+" with value "+value);
395 if (value instanceof String) {
396 value = ((String) value).trim();
397 }
398 String typeName = entry.getAttribute("type");
399 Integer typeObject = (Integer) AbstractDatabaseAction.typeConstants.get(typeName);
400 if (typeObject == null) {
401 throw new SQLException("Can't set column because the type "+typeName+" is unrecognized");
402 }
403 if (value == null) {
404 /** If the value is null, set the column value null and return **/
405 if (typeName.equals("image-width") || typeName.equals("image-height") || typeName.equals("image-size") || typeName.equals("row-index") || typeName.equals("image-mime-type")) {
406 /** these column types are automatically generated so it's ok **/
407 } else {
408 statement.setNull(position, typeObject.intValue());
409 return;
410 }
411 }
412 if ("".equals(value)) {
413 switch (typeObject.intValue()) {
414 case Types.CHAR:
415 case Types.CLOB:
416 case Types.VARCHAR:
417 /** If the value is an empty string and the column is
418 a string type, we can continue **/
419 break;
420 case Types.INTEGER:
421 if (typeName.equals("image-width") || typeName.equals("image-height") || typeName.equals("image-size") || typeName.equals("row-index")) {
422 /** again, these types are okay to be absent **/
423 break;
424 }
425 default:
426 /** If the value is an empty string and the column
427 is something else, we treat it as a null value **/
428 statement.setNull(position, typeObject.intValue());
429 return;
430 }
431 }
432
433 /** Store the column value in the request attribute
434 keyed by the request parameter name. we do this so possible future
435 actions can access this data. not sure about the key tho... **/
436 setRequestAttribute(request,param,value);
437 File file;
438
439 switch (typeObject.intValue()) {
440 case Types.CLOB:
441 int length = -1;
442 InputStream asciiStream = null;
443
444 if (value instanceof File) {
445 File asciiFile = (File) value;
446 asciiStream = new BufferedInputStream(new FileInputStream(asciiFile));
447 length = (int) asciiFile.length();
448 } else {
449 String asciiText = (String) value;
450 asciiStream = new BufferedInputStream(new ByteArrayInputStream(asciiText.getBytes()));
451 length = asciiText.length();
452 }
453
454 statement.setAsciiStream(position, asciiStream, length);
455 break;
456 case Types.BIGINT:
457 BigDecimal bd = null;
458
459 if (value instanceof BigDecimal) {
460 bd = (BigDecimal) value;
461 } else {
462 bd = new BigDecimal((String) value);
463 }
464
465 statement.setBigDecimal(position, bd);
466 break;
467 case Types.TINYINT:
468 Byte b = null;
469
470 if (value instanceof Byte) {
471 b = (Byte) value;
472 } else {
473 b = new Byte((String) value);
474 }
475
476 statement.setByte(position, b.byteValue());
477 break;
478 case Types.DATE:
479 Date d = null;
480
481 if (value instanceof Date) {
482 d = (Date) value;
483 } else if (value instanceof java.util.Date) {
484 d = new Date(((java.util.Date) value).getTime());
485 } else {
486 d = new Date(this.dateValue((String) value, entry.getAttribute("format", "M/d/yyyy")));
487 }
488
489 statement.setDate(position, d);
490 break;
491 case Types.DOUBLE:
492 Double db = null;
493
494 if (value instanceof Double) {
495 db = (Double) value;
496 } else {
497 db = new Double((String) value);
498 }
499
500 statement.setDouble(position, db.doubleValue());
501 break;
502 case Types.FLOAT:
503 Float f = null;
504
505 if (value instanceof Float) {
506 f = (Float) value;
507 } else {
508 f = new Float((String) value);
509 }
510
511 statement.setFloat(position, f.floatValue());
512 break;
513 case Types.NUMERIC:
514 Long l = null;
515
516 if (value instanceof Long) {
517 l = (Long) value;
518 } else {
519 l = new Long((String) value);
520 }
521
522 statement.setLong(position, l.longValue());
523 break;
524 case Types.SMALLINT:
525 Short s = null;
526
527 if (value instanceof Short) {
528 s = (Short) value;
529 } else {
530 s = new Short((String) value);
531 }
532
533 statement.setShort(position, s.shortValue());
534 break;
535 case Types.TIME:
536 Time t = null;
537
538 if (value instanceof Time) {
539 t = (Time) value;
540 } else {
541 t = new Time(this.dateValue((String) value, entry.getAttribute("format", "h:m:s a")));
542 }
543
544 statement.setTime(position, t);
545 break;
546 case Types.TIMESTAMP:
547 Timestamp ts = null;
548
549 if (value instanceof Time) {
550 ts = (Timestamp) value;
551 } else {
552 ts = new Timestamp(this.dateValue((String) value, entry.getAttribute("format", "M/d/yyyy h:m:s a")));
553 }
554
555 statement.setTimestamp(position, ts);
556 break;
557 case Types.ARRAY:
558 statement.setArray(position, (Array) value); // no way to convert string to array
559 break;
560 case Types.STRUCT:
561 case Types.OTHER:
562 statement.setObject(position, value);
563 break;
564 case Types.LONGVARBINARY:
565 statement.setTimestamp(position, new Timestamp((new java.util.Date()).getTime()));
566 break;
567 case Types.VARCHAR:
568 if ("string".equals(typeName)) {
569 statement.setString(position, (String) value);
570 break;
571 } else if ("image-mime-type".equals(typeName)) {
572 String imageAttr = param.substring(0, (param.length() - "-mime-type".length()));
573 file = (File) request.get(imageAttr);
574 synchronized (this.files) {
575 Parameters parameters = (Parameters) this.files.get(file);
576 String imageMimeType = parameters.getParameter("image-mime-type",
577 (String) settings.get("image-mime-type",""));
578 statement.setString(position, imageMimeType);
579 /** Store the image mime type in the request attributes.
580 Why do we do this? **/
581 setRequestAttribute(request, param, imageMimeType);
582 }
583 break;
584 }
585 case Types.BLOB:
586 if (value instanceof File) {
587 file = (File)value;
588 } else if (value instanceof String) {
589 file = new File((String)value);
590 } else {
591 throw new SQLException("Invalid type for blob: "+value.getClass().getName());
592 }
593 //InputStream input = new BufferedInputStream(new FileInputStream(file));
594 FileInputStream input = new FileInputStream(file);
595 statement.setBinaryStream(position, input, (int)file.length());
596 if ("image".equals(typeName)) {
597 /** If this column type is an image, store the
598 size, width, and height in a static table **/
599 Parameters parameters = new Parameters();
600 parameters.setParameter("image-size", Long.toString(file.length()));
601 ImageProperties prop = ImageUtils.getImageProperties(file);
602 parameters.setParameter("image-width", Integer.toString(prop.width));
603 parameters.setParameter("image-height", Integer.toString(prop.height));
604 // TC: if it's really mime-type shouldn't we prepend "image/"?
605 parameters.setParameter("image-mime-type",prop.type);
606 synchronized (this.files) {
607 this.files.put(file, parameters);
608 }
609 }
610 break;
611 case Types.INTEGER:
612 if ("int".equals(typeName)) {
613 Integer i = null;
614 if (value instanceof Integer) {
615 i = (Integer) value;
616 } else {
617 i = new Integer((String) value);
618 }
619 statement.setInt(position, i.intValue());
620 break;
621 } else if ("image-width".equals(typeName)) {
622 /** Get the image width from the cached image data **/
623 /** Is this why we store the values in the request
624 attributes? **/
625 String imageAttr = param.substring(0, (param.length() - "-width".length()));
626 file = (File) request.get(imageAttr);
627 synchronized (this.files) {
628 Parameters parameters = (Parameters) this.files.get(file);
629 statement.setInt(position, parameters.getParameterAsInteger("image-width",
630 Integer.parseInt((String)settings.get("image-width","-1"))));
631 /** Store the image width in the request attributes.
632 Why do we do this? **/
633 setRequestAttribute(request,
634 param,
635 parameters.getParameter("image-width",
636 (String) settings.get("image-width","")));
637 }
638 break;
639 } else if ("image-height".equals(typeName)) {
640 /** Get the image height from the cached image data **/
641 String imageAttr = param.substring(0, (param.length() - "-height".length()));
642 file = (File) request.get(imageAttr);
643 synchronized (this.files) {
644 Parameters parameters = (Parameters) this.files.get(file);
645 statement.setInt(position, parameters.getParameterAsInteger("image-height",
646 Integer.parseInt((String)settings.get("image-height","-1"))));
647 setRequestAttribute(request,
648 param,
649 parameters.getParameter("image-height",
650 (String) settings.get("image-height","")));
651 }
652 break;
653 } else if ("image-size".equals(typeName)) {
654 /** Get the image file size from the cached image data **/
655 String imageAttr = param.substring(0, (param.length() - "-size".length()));
656 file = (File) request.get(imageAttr);
657 synchronized (this.files) {
658 Parameters parameters = (Parameters) this.files.get(file);
659 statement.setInt(position, parameters.getParameterAsInteger("image-size",
660 Integer.parseInt((String)settings.get("image-height","-1"))));
661 setRequestAttribute(request,
662 param,
663 parameters.getParameter("image-size",
664 (String) settings.get("image-size","")));
665 }
666 break;
667 } else if ("row-index".equals(typeName)) {
668 statement.setInt(position,rowIndex);
669 break;
670 }
671 default:
672 throw new SQLException("Impossible exception - invalid type "+typeName);
673 }
674 }
675
676 /**
677 * Convert a String to a long value.
678 */
679 private final long dateValue(String value, String format) throws Exception {
680 DateFormat formatter = new SimpleDateFormat(format);
681 return formatter.parse(value).getTime();
682 }
683
684 /**
685 * dispose
686 */
687 public void dispose() {
688 this.manager.release(dbselector);
689 }
690
691 /**
692 * Store a key/value pair in the request attributes. We prefix the key
693 * with the name of this class to prevent potential name collisions.
694 */
695 protected void setRequestAttribute(Request request, String key, Object value) {
696 request.setAttribute("org.apache.cocoon.acting.AbstractDatabaseAction:"+key,value);
697 }
698
699 /**
700 * Retreive a value from the request attributes.
701 */
702 protected Object getRequestAttribute(Request request, String key) {
703 return request.getAttribute("org.apache.cocoon.acting.AbstractDatabaseAction:"+key);
704 }
705
706 /**
707 * Build a separed list with the Values of a Configuration Array
708 * @param values - build the list from
709 * @param separator - Put a separator between the values of the list
710 * @return - an StringBuffer with the builded List
711 * @throws ConfigurationException
712 */
713 protected StringBuffer buildList(Configuration[] values, String separator) throws ConfigurationException {
714 StringBuffer buffer = new StringBuffer();
715 for (int i = 0; i < values.length; i++) {
716 if (i > 0) {
717 buffer.append(separator);
718 }
719 buffer.append(values[i].getAttribute("dbcol"));
720 buffer.append(" = ?");
721 }
722 return buffer;
723 }
724
725 /**
726 * Build a separed list with the Values of a Configuration Array
727 * @param values - build the list from
728 * @param begin - Initial index
729 * @return - an StringBuffer with the builded List
730 * @throws ConfigurationException
731 */
732 protected StringBuffer buildList(Configuration[] values, int begin) throws ConfigurationException {
733 StringBuffer buffer = new StringBuffer();
734 int length = values.length;
735 boolean prependComma = begin > 0;
736 for (int i = 0; i < length; i++) {
737 if (prependComma) {
738 buffer.append(", ");
739 } else {
740 prependComma = true;
741 }
742 buffer.append(values[i].getAttribute("dbcol"));
743 }
744 return buffer;
745 }
746 }