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
18 package org.apache.cocoon.acting.modular;
19
20 import java.io.IOException;
21 import java.sql.Connection;
22 import java.sql.PreparedStatement;
23 import java.sql.SQLException;
24 import java.util.Map;
25
26 import org.apache.avalon.excalibur.datasource.DataSourceComponent;
27 import org.apache.avalon.framework.activity.Disposable;
28 import org.apache.avalon.framework.configuration.Configuration;
29 import org.apache.avalon.framework.configuration.ConfigurationException;
30 import org.apache.avalon.framework.parameters.Parameters;
31 import org.apache.avalon.framework.service.ServiceException;
32 import org.apache.avalon.framework.service.ServiceManager;
33 import org.apache.avalon.framework.service.ServiceSelector;
34 import org.apache.avalon.framework.thread.ThreadSafe;
35
36 import org.apache.cocoon.Constants;
37 import org.apache.cocoon.ProcessingException;
38 import org.apache.cocoon.acting.AbstractComplementaryConfigurableAction;
39 import org.apache.cocoon.components.modules.database.AutoIncrementModule;
40 import org.apache.cocoon.components.modules.input.InputModule;
41 import org.apache.cocoon.components.modules.output.OutputModule;
42 import org.apache.cocoon.environment.Redirector;
43 import org.apache.cocoon.environment.SourceResolver;
44 import org.apache.cocoon.util.HashMap;
45 import org.apache.cocoon.util.JDBCTypeConversions;
46 import org.apache.commons.lang.BooleanUtils;
47
48 /**
49 * Abstract action for common function needed by database actions.
50 * The difference to the other Database*Actions is, that the actions
51 * in this package use additional components ("modules") for reading
52 * and writing parameters. In addition the descriptor format has
53 * changed to accomodate the new features.
54 *
55 * <p>This action is heavily based upon the original DatabaseAddActions.</p>
56 *
57 * <p>Modes have to be configured in cocoon.xconf. Mode names from
58 * descriptor.xml file are looked up in the component service. Default
59 * mode names can only be set during action setup. </p>
60 *
61 * <p>The number of affected rows is returned to the sitemap with the
62 * "row-count" parameter if at least one row was affected.</p>
63 *
64 * <p>All known column types can be found in
65 * {@link org.apache.cocoon.util.JDBCTypeConversions JDBCTypeConversions}.</p>
66 *
67 * <table>
68 * <tr><td colspan="2">Configuration options (setup):</td></tr>
69 * <tr><td>input </td><td>default mode name for reading values (request-param)</td></tr>
70 * <tr><td>autoincrement </td><td>default mode name for obtaining values from autoincrement columns (auto)</td></tr>
71 * <tr><td>append-row </td><td>append row number in square brackets to column name for output (yes)</td></tr>
72 * <tr><td>append-table-name</td><td>add table name to column name for both in- and output (yes)</td></tr>
73 * <tr><td>first-row </td><td>row index of first row (0)</td></tr>
74 * <tr><td>path-separator </td><td>string to separate table name from column name (.)</td></tr>
75 * </table>
76 *
77 * <table>
78 * <tr><td colspan="2">Configuration options (setup and per invocation):</td></tr>
79 * <tr><td>throw-exception </td><td>throw an exception when an error occurs (default: false)</td></tr>
80 * <tr><td>descriptor </td><td>file containing database description</td></tr>
81 * <tr><td>table-set </td><td>table-set name to work with </td></tr>
82 * <tr><td>output </td><td>mode name for writing values (request-attr)</td></tr>
83 * <tr><td>reloadable </td><td>dynamically reload descriptor file if change is detected</td></tr>
84 * <tr><td>use-transactions </td><td>defaults to yes</td></tr>
85 * <tr><td>connection </td><td>configured datasource connection to use (overrides value from descriptor file)</td></tr>
86 * <tr><td>fail-on-empty </td><td>(boolean) fail is statement affected zero rows (true)</td></tr>
87 * </table>
88 *
89 * @author <a href="mailto:haul@apache.org">Christian Haul</a>
90 * @version $Id: DatabaseAction.java 433543 2006-08-22 06:22:54Z crossley $
91 * @see org.apache.cocoon.components.modules.input
92 * @see org.apache.cocoon.components.modules.output
93 * @see org.apache.cocoon.components.modules.database
94 * @see org.apache.cocoon.util.JDBCTypeConversions
95 */
96 public abstract class DatabaseAction extends AbstractComplementaryConfigurableAction implements Disposable, ThreadSafe {
97
98 // ========================================================================
99 // constants
100 // ========================================================================
101
102 static final Integer MODE_AUTOINCR = new Integer(0);
103 static final Integer MODE_OTHERS = new Integer(1);
104 static final Integer MODE_OUTPUT = new Integer(2);
105
106 static final String ATTRIBUTE_KEY = "org.apache.cocoon.action.modular.DatabaseAction.outputModeName";
107
108 // These can be overidden from cocoon.xconf
109 static final String inputHint = "request-param"; // default to request parameters
110 static final String outputHint = "request-attr"; // default to request attributes
111 static final String databaseHint = "manual"; // default to manual auto increments
112
113 static final String INPUT_MODULE_SELECTOR = InputModule.ROLE + "Selector";
114 static final String OUTPUT_MODULE_SELECTOR = OutputModule.ROLE + "Selector";
115 static final String DATABASE_MODULE_SELECTOR = AutoIncrementModule.ROLE + "Selector";
116
117
118 // ========================================================================
119 // instance vars
120 // ========================================================================
121
122 protected ServiceSelector dbselector;
123 protected Map defaultModeNames = new HashMap( 3 );
124 protected final HashMap cachedQueryData = new HashMap();
125 protected String pathSeparator = ".";
126 protected int firstRow = 0;
127 protected boolean failOnEmpty = true;
128
129 // ========================================================================
130 // inner helper classes
131 // ========================================================================
132
133 /**
134 * Structure that takes all processed data for one column.
135 */
136 protected static class Column {
137 boolean isKey = false;
138 boolean isSet = false;
139 boolean isAutoIncrement = false;
140 String mode = null;
141 Configuration modeConf = null;
142 Configuration columnConf = null;
143 }
144
145 /**
146 * Structure that takes all processed data for a table depending
147 * on current default modes
148 */
149 protected static class CacheHelper {
150 /**
151 * Generated query string
152 */
153 public String queryString = null;
154 /**
155 * if a set is used, column number which is used to determine
156 * the number of rows to insert.
157 */
158 public int setMaster = -1;
159 public boolean isSet = false;
160 public int noOfKeys = 0;
161 public Column[] columns = null;
162
163 public CacheHelper( int cols ) {
164 this(0,cols);
165 }
166
167 public CacheHelper( int keys, int cols ) {
168 noOfKeys = keys;
169 columns = new Column[cols];
170 for ( int i=0; i<cols; i++ ) {
171 columns[i] = new Column();
172 }
173 }
174 }
175
176 /**
177 * Structure that takes up both current mode types for database
178 * operations and table configuration data. Used to access parsed
179 * configuration data.
180 */
181 protected static class LookUpKey {
182 public Configuration tableConf = null;
183 public Map modeTypes = null;
184
185 public LookUpKey( Configuration tableConf, Map modeTypes ) {
186 this.tableConf = tableConf;
187 this.modeTypes = modeTypes;
188 }
189
190 /* (non-Javadoc)
191 * @see java.lang.Object#equals(java.lang.Object)
192 */
193 public boolean equals(Object obj) {
194 boolean result = false;
195 if (obj != null && obj instanceof LookUpKey) {
196 LookUpKey luk = (LookUpKey) obj;
197 result = true;
198 result = result && (luk.tableConf == null ?
199 this.tableConf == null : luk.tableConf.equals(this.tableConf));
200 result = result && (luk.modeTypes == null ?
201 this.modeTypes == null : luk.modeTypes.equals(this.modeTypes));
202 }
203
204 return result;
205 }
206
207 /* (non-Javadoc)
208 * @see java.lang.Object#hashCode()
209 */
210 public int hashCode() {
211 return (this.tableConf != null ?
212 this.tableConf.hashCode() :
213 (this.modeTypes != null ? this.modeTypes.hashCode() : super.hashCode()));
214 }
215 }
216
217 // set up default modes
218 // <input/>
219 // <output/>
220 // <autoincrement/>
221 //
222 // all other modes need to be declared in cocoon.xconf
223 // no need to declare them per action (anymore!)
224 public void configure(Configuration conf) throws ConfigurationException {
225 super.configure(conf);
226 if (this.settings != null) {
227 this.defaultModeNames.put(MODE_OTHERS, this.settings.get("input", inputHint));
228 this.defaultModeNames.put(MODE_OUTPUT, this.settings.get("output", outputHint));
229 this.defaultModeNames.put(MODE_AUTOINCR, this.settings.get("autoincrement", databaseHint));
230 this.pathSeparator = (String)this.settings.get("path-separator", this.pathSeparator);
231 String tmp = (String)this.settings.get("first-row",null);
232 if (tmp != null) {
233 try {
234 this.firstRow = Integer.parseInt(tmp);
235 } catch (NumberFormatException nfe) {
236 if (getLogger().isWarnEnabled())
237 getLogger().warn("problem parsing first row option "+tmp+" using default instead.");
238 }
239 }
240 tmp = (String) this.settings.get("fail-on-empty",String.valueOf(this.failOnEmpty));
241 this.failOnEmpty = BooleanUtils.toBoolean(tmp);
242 }
243 }
244
245 // ========================================================================
246 // Avalon methods
247 // ========================================================================
248
249 /**
250 * Compose the Actions so that we can select our databases.
251 */
252 public void service(ServiceManager manager) throws ServiceException {
253 super.service(manager);
254 this.dbselector = (ServiceSelector) manager.lookup(DataSourceComponent.ROLE + "Selector");
255 }
256
257 /**
258 * dispose
259 */
260 public void dispose() {
261 this.manager.release(dbselector);
262 }
263
264 // ========================================================================
265 // protected utility methods
266 // ========================================================================
267
268 /**
269 * Get the Datasource we need.
270 */
271 protected DataSourceComponent getDataSource( Configuration conf, Parameters parameters )
272 throws ServiceException {
273
274 String sourceName = parameters.getParameter( "connection", (String) settings.get( "connection" ) );
275 if ( sourceName == null ) {
276 Configuration dsn = conf.getChild("connection");
277 return (DataSourceComponent) this.dbselector.select(dsn.getValue(""));
278 } else {
279 if (getLogger().isDebugEnabled())
280 getLogger().debug("Using datasource: "+sourceName);
281 return (DataSourceComponent) this.dbselector.select(sourceName);
282 }
283 }
284
285 /**
286 * Return whether a type is a Large Object (BLOB/CLOB).
287 */
288 protected final boolean isLargeObject (String type) {
289 if ("ascii".equals(type)) return true;
290 if ("binary".equals(type)) return true;
291 if ("image".equals(type)) return true;
292 return false;
293 }
294
295 /**
296 * Store a key/value pair in the output attributes. We prefix the key
297 * with the name of this class to prevent potential name collisions.
298 */
299 protected void setOutputAttribute(Map objectModel, String outputMode, String key, Object value) {
300
301 ServiceSelector outputSelector = null;
302 OutputModule output = null;
303 try {
304 outputSelector = (ServiceSelector) this.manager.lookup(OUTPUT_MODULE_SELECTOR);
305 if (outputMode != null && outputSelector != null && outputSelector.isSelectable(outputMode)) {
306 output = (OutputModule) outputSelector.select(outputMode);
307 }
308 if (output != null) {
309 output.setAttribute(null, objectModel, key, value);
310 } else if (getLogger().isWarnEnabled()) {
311 getLogger().warn("Could not select output mode " + outputMode);
312 }
313 } catch (Exception e) {
314 if (getLogger().isWarnEnabled()) {
315 getLogger().warn("Could not select output mode " + outputMode + ":" + e.getMessage());
316 }
317 } finally {
318 if (outputSelector != null) {
319 if (output != null)
320 outputSelector.release(output);
321 this.manager.release(outputSelector);
322 }
323 }
324 }
325
326 /**
327 * Inserts a row or a set of rows into the given table based on the
328 * request parameters
329 *
330 * @param table the table's configuration
331 * @param conn the database connection
332 * @param objectModel the objectModel
333 */
334 protected int processTable( Configuration table, Connection conn, Map objectModel,
335 Map results, Map modeTypes )
336 throws SQLException, ConfigurationException, Exception {
337
338 PreparedStatement statement = null;
339 int rows = 0;
340 try {
341 LookUpKey luk = new LookUpKey(table, modeTypes);
342 CacheHelper queryData = null;
343
344 if (getLogger().isDebugEnabled())
345 getLogger().debug("modeTypes : "+ modeTypes);
346
347 // get cached data
348 // synchronize complete block since we don't want 100s of threads
349 // generating the same cached data set. In the long run all data
350 // is cached anyways so this won't cost much.
351 synchronized (this.cachedQueryData) {
352 queryData = (CacheHelper) this.cachedQueryData.get(luk,null);
353 if (queryData == null) {
354 queryData = this.getQuery( table, modeTypes, defaultModeNames );
355 this.cachedQueryData.put(luk,queryData);
356 }
357 }
358
359 if (getLogger().isDebugEnabled())
360 getLogger().debug("query: "+queryData.queryString);
361 statement = conn.prepareStatement(queryData.queryString);
362
363 Object[][] columnValues = this.getColumnValues( table, queryData, objectModel );
364
365 int setLength = 1;
366 if ( queryData.isSet ) {
367 if ( columnValues[ queryData.setMaster ] != null ) {
368 setLength = columnValues[ queryData.setMaster ].length;
369 } else {
370 setLength = 0;
371 }
372 }
373
374 for ( int rowIndex = 0; rowIndex < setLength; rowIndex++ ) {
375 if (getLogger().isDebugEnabled()) {
376 getLogger().debug( "====> row no. " + rowIndex );
377 }
378 rows += processRow( objectModel, conn, statement, (String) modeTypes.get(MODE_OUTPUT), table, queryData, columnValues, rowIndex, results );
379 }
380 } finally {
381 try {
382 if (statement != null) {
383 statement.close();
384 }
385 } catch (SQLException e) {}
386 }
387 return rows;
388 }
389
390 /**
391 * Choose a mode configuration based on its name.
392 * @param conf Configuration (i.e. a column's configuration) that might have
393 * several children configurations named "mode".
394 * @param type desired type (i.e. every mode has a type
395 * attribute), find the first mode that has a compatible type.
396 * Special mode "all" matches all queried types.
397 * @return configuration that has desired type or type "all" or null.
398 */
399 protected Configuration getMode( Configuration conf, String type )
400 throws ConfigurationException {
401
402 String modeAll = "all";
403 Configuration[] modes = conf.getChildren("mode");
404 Configuration modeConfig = null;
405
406 for ( int i=0; i<modes.length; i++ ) {
407 String modeType = modes[i].getAttribute("type", "others");
408 if ( modeType.equals(type) || modeType.equals(modeAll)) {
409 if (getLogger().isDebugEnabled())
410 getLogger().debug("requested mode was \""+type+"\" returning \""+modeType+"\"");
411 modeConfig = modes[i];
412 break;
413 }
414 }
415 return modeConfig;
416 }
417
418 /**
419 * compose name for output a long the lines of "table.column"
420 */
421 protected String getOutputName ( Configuration tableConf, Configuration columnConf ) {
422
423 return getOutputName( tableConf, columnConf, -1 );
424 }
425
426 /**
427 * compose name for output a long the lines of "table.column[row]" or
428 * "table.column" if rowIndex is -1.
429 * If the section of the sitemap corresponding to the action contains
430 * <append-table-name>false</append-table-name>
431 * the name for output is "column[row]"
432 * If the section of the sitemap corresponding to the action contains
433 * <append-row>false</append-row>
434 * the name for output is "column"
435 */
436 protected String getOutputName ( Configuration tableConf, Configuration columnConf, int rowIndex ) {
437
438 if ( rowIndex != -1 && this.settings.containsKey("append-row") &&
439 (this.settings.get("append-row").toString().equalsIgnoreCase("false") ||
440 this.settings.get("append-row").toString().equalsIgnoreCase("0")) ) {
441 rowIndex = -1;
442 } else {
443 rowIndex = rowIndex + this.firstRow;
444 }
445 if ( this.settings.containsKey("append-table-name") &&
446 (this.settings.get("append-table-name").toString().equalsIgnoreCase("false") ||
447 this.settings.get("append-table-name").toString().equalsIgnoreCase("0")) )
448 {
449 return ( columnConf.getAttribute("name",null)
450 + ( rowIndex == -1 ? "" : "[" + rowIndex + "]" ) );
451 } else {
452 return ( tableConf.getAttribute("alias", tableConf.getAttribute("name", null) )
453 + this.pathSeparator + columnConf.getAttribute("name",null)
454 + ( rowIndex == -1 ? "" : "[" + rowIndex + "]" ) );
455 }
456 }
457
458 /*
459 * Read all values for a column from an InputModule
460 *
461 * If the given column is an autoincrement column, an empty array
462 * is returned, otherwise if it is part of a set, all available
463 * values are fetched, or only the first one if it is not part of
464 * a set.
465 *
466 */
467 protected Object[] getColumnValue(Configuration tableConf, Column column, Map objectModel)
468 throws ConfigurationException, ServiceException {
469
470 if (column.isAutoIncrement) {
471 return new Object[1];
472 } else {
473 Object[] values;
474 String cname = getOutputName( tableConf, column.columnConf );
475
476 // obtain input module and read values
477 ServiceSelector inputSelector = null;
478 InputModule input = null;
479 try {
480 inputSelector = (ServiceSelector) this.manager.lookup(INPUT_MODULE_SELECTOR);
481 if (column.mode != null && inputSelector != null && inputSelector.isSelectable(column.mode)){
482 input = (InputModule) inputSelector.select(column.mode);
483 }
484
485 if (column.isSet) {
486 if (getLogger().isDebugEnabled()) {
487 getLogger().debug( "Trying to set column " + cname + " from " + column.mode + " using getAttributeValues method");
488 }
489 values = input.getAttributeValues( cname, column.modeConf, objectModel );
490 } else {
491 if (getLogger().isDebugEnabled()) {
492 getLogger().debug( "Trying to set column " + cname + " from " + column.mode + " using getAttribute method");
493 }
494 values = new Object[1];
495 values[0] = input.getAttribute( cname, column.modeConf, objectModel );
496 }
497
498 if (values != null) {
499 for ( int i = 0; i < values.length; i++ ) {
500 if (getLogger().isDebugEnabled()) {
501 getLogger().debug( "Setting column " + cname + " [" + i + "] " + values[i] );
502 }
503 }
504 }
505 } finally {
506 if (inputSelector != null) {
507 if (input != null) {
508 inputSelector.release(input);
509 }
510 this.manager.release(inputSelector);
511 }
512 }
513 return values;
514 }
515 }
516
517 /**
518 * Setup parsed attribute configuration object
519 */
520 protected void fillModes ( Configuration[] conf, boolean isKey, Map defaultModeNames,
521 Map modeTypes, CacheHelper set )
522 throws ConfigurationException {
523
524 String setMode = null;
525 int offset = (isKey ? 0: set.noOfKeys);
526
527 for (int i = offset; i < conf.length + offset; i++) {
528 if (getLogger().isDebugEnabled()) {
529 getLogger().debug("i=" + i);
530 }
531 set.columns[i].columnConf = conf[ i - offset ];
532 set.columns[i].isSet = false;
533 set.columns[i].isKey = isKey;
534 set.columns[i].isAutoIncrement = false;
535 if (isKey & this.honourAutoIncrement()) {
536 set.columns[i].isAutoIncrement = set.columns[i].columnConf.getAttributeAsBoolean("autoincrement",false);
537 }
538 set.columns[i].modeConf = getMode(set.columns[i].columnConf,
539 selectMode(set.columns[i].isAutoIncrement, modeTypes));
540 set.columns[i].mode = (set.columns[i].modeConf != null ?
541 set.columns[i].modeConf.getAttribute("name", selectMode(isKey, defaultModeNames)) :
542 selectMode(isKey, defaultModeNames));
543 // Determine set mode for a whole column ...
544 setMode = set.columns[i].columnConf.getAttribute("set", null); // master vs slave vs null
545 if (setMode == null && set.columns[i].modeConf != null) {
546 // ... or for each mode individually
547 setMode = set.columns[i].modeConf.getAttribute("set", null);
548 }
549 if (setMode != null) {
550 set.columns[i].isSet = true;
551 set.isSet = true;
552 if (setMode.equals("master")) {
553 set.setMaster = i;
554 }
555 }
556 }
557 }
558
559 /**
560 * create a unique name using the getOutputName method and write
561 * the value to the output module and the results map if present.
562 *
563 */
564 protected void setOutput( Map objectModel, String outputMode, Map results,
565 Configuration table, Configuration column, int rowIndex, Object value ) {
566
567 String param = this.getOutputName( table, column, rowIndex );
568 if (getLogger().isDebugEnabled()) {
569 getLogger().debug( "Setting column " + param + " to " + value );
570 }
571 this.setOutputAttribute(objectModel, outputMode, param, value);
572 if (results != null) {
573 results.put( param, String.valueOf( value ) );
574 }
575 }
576
577 /**
578 * set a column in a statement using the appropriate JDBC setXXX method.
579 *
580 */
581 protected void setColumn (PreparedStatement statement, int position, Configuration entry, Object value) throws Exception {
582 JDBCTypeConversions.setColumn(statement, position, value,
583 (Integer)JDBCTypeConversions.typeConstants.get(entry.getAttribute("type")));
584 }
585
586 /**
587 * set a column in a statement using the appropriate JDBC setXXX
588 * method and propagate the value to the output module and results
589 * map if present. Effectively combines calls to setColumn and
590 * setOutput.
591 *
592 */
593 protected void setColumn (Map objectModel, String outputMode, Map results,
594 Configuration table, Configuration column, int rowIndex,
595 Object value, PreparedStatement statement, int position) throws Exception {
596
597 if (results != null) {
598 this.setOutput(objectModel, outputMode, results, table, column, rowIndex, value);
599 }
600 this.setColumn( statement, position, column, value );
601 }
602
603 // ========================================================================
604 // main method
605 // ========================================================================
606
607 /**
608 * Add a record to the database. This action assumes that
609 * the file referenced by the "descriptor" parameter conforms
610 * to the AbstractDatabaseAction specifications.
611 */
612 public Map act(Redirector redirector, SourceResolver resolver, Map objectModel,
613 String source, Parameters param) throws Exception {
614
615 DataSourceComponent datasource = null;
616 Connection conn = null;
617 Map results = new HashMap();
618 int rows = 0;
619 boolean failed = false;
620
621 // read global parameter settings
622 boolean reloadable = Constants.DESCRIPTOR_RELOADABLE_DEFAULT;
623
624 // call specific default modes apart from output mode are not supported
625 // set request attribute
626 String outputMode = param.getParameter("output", (String) defaultModeNames.get(MODE_OUTPUT));
627
628 if (this.settings.containsKey("reloadable")) {
629 reloadable = Boolean.valueOf((String) this.settings.get("reloadable")).booleanValue();
630 }
631 // read local parameter settings
632 try {
633 Configuration conf =
634 this.getConfiguration(param.getParameter("descriptor", (String) this.settings.get("descriptor")),
635 resolver,
636 param.getParameterAsBoolean("reloadable",reloadable));
637
638 // get database connection and try to turn off autocommit
639 datasource = this.getDataSource(conf, param);
640 conn = datasource.getConnection();
641 if (conn.getAutoCommit() == true) {
642 try {
643 conn.setAutoCommit(false);
644 } catch (Exception ex) {
645 String tmp = param.getParameter("use-transactions",(String) this.settings.get("use-transactions",null));
646 if (tmp != null && (tmp.equalsIgnoreCase("no") || tmp.equalsIgnoreCase("false") || tmp.equalsIgnoreCase("0"))) {
647 if (getLogger().isErrorEnabled())
648 getLogger().error("This DB connection does not support transactions. If you want to risk your data's integrity by continuing nonetheless set parameter \"use-transactions\" to \"no\".");
649 throw ex;
650 }
651 }
652 }
653
654 // find tables to work with
655 Configuration[] tables = conf.getChildren("table");
656 String tablesetname = param.getParameter("table-set", (String) this.settings.get("table-set"));
657
658 Map modeTypes = null;
659
660 if (tablesetname == null) {
661 modeTypes = new HashMap(6);
662 modeTypes.put( MODE_AUTOINCR, "autoincr" );
663 modeTypes.put( MODE_OTHERS, "others" );
664 modeTypes.put( MODE_OUTPUT, outputMode );
665 for (int i = 0; i < tables.length; i++) {
666 rows += processTable(tables[i], conn, objectModel, results, modeTypes);
667 }
668 } else {
669 // new set based behaviour
670
671 // create index for table names / aliases
672 Map tableIndex = new HashMap(2*tables.length);
673 String tableName = null;
674 Object result = null;
675 for (int i=0; i<tables.length; i++) {
676 tableName = tables[i].getAttribute("alias",tables[i].getAttribute("name",""));
677 result = tableIndex.put(tableName,new Integer(i));
678 if (result != null) {
679 throw new IOException("Duplicate table entry for "+tableName+" at positions "+result+" and "+i);
680 }
681 }
682
683 Configuration[] tablesets = conf.getChildren("table-set");
684 String setname = null;
685 boolean found = false;
686
687 // find tables contained in tableset
688 int j = 0;
689 for (j = 0; j < tablesets.length; j++) {
690 setname = tablesets[j].getAttribute ("name", "");
691 if (tablesetname.trim().equals (setname.trim ())) {
692 found = true;
693 break;
694 }
695 }
696 if (!found) {
697 throw new IOException(" given set " + tablesetname + " does not exists in a description file.");
698 }
699
700 Configuration[] set = tablesets[j].getChildren("table");
701
702 for (int i = 0; i < set.length; i++) {
703 // look for alternative mode types
704 modeTypes = new HashMap(6);
705 modeTypes.put( MODE_AUTOINCR, set[i].getAttribute( "autoincr-mode", "autoincr" ) );
706 modeTypes.put( MODE_OTHERS, set[i].getAttribute( "others-mode", "others" ) );
707 modeTypes.put( MODE_OUTPUT, outputMode );
708 tableName=set[i].getAttribute("name","");
709 if (tableIndex.containsKey(tableName)) {
710 j = ((Integer)tableIndex.get(tableName)).intValue();
711 rows += processTable( tables[j], conn, objectModel, results, modeTypes );
712 } else {
713 throw new IOException(" given table " + tableName + " does not exists in a description file.");
714 }
715 }
716 }
717
718 if (conn.getAutoCommit() == false) {
719 conn.commit();
720 }
721
722 // obtain output mode module and rollback output
723 ServiceSelector outputSelector = null;
724 OutputModule output = null;
725 try {
726 outputSelector = (ServiceSelector) this.manager.lookup(OUTPUT_MODULE_SELECTOR);
727 if (outputMode != null && outputSelector != null && outputSelector.isSelectable(outputMode)){
728 output = (OutputModule) outputSelector.select(outputMode);
729 }
730 if (output != null) {
731 output.commit(null, objectModel);
732 } else if (getLogger().isWarnEnabled()) {
733 getLogger().warn("Could not select output mode " + outputMode);
734 }
735 } catch (ServiceException e) {
736 if (getLogger().isWarnEnabled()) {
737 getLogger().warn("Could not select output mode " + outputMode + ":" + e.getMessage());
738 }
739 } finally {
740 if (outputSelector != null) {
741 if (output != null) {
742 outputSelector.release(output);
743 }
744 this.manager.release(outputSelector);
745 }
746 }
747 } catch (Exception e) {
748 failed = true;
749 if ( conn != null ) {
750 try {
751 if (getLogger().isDebugEnabled()) {
752 getLogger().debug( "Rolling back transaction. Caused by " + e.getMessage() );
753 e.printStackTrace();
754 }
755 conn.rollback();
756 results = null;
757
758 // obtain output mode module and commit output
759 ServiceSelector outputSelector = null;
760 OutputModule output = null;
761 try {
762 outputSelector = (ServiceSelector) this.manager.lookup(OUTPUT_MODULE_SELECTOR);
763 if (outputMode != null && outputSelector != null && outputSelector.isSelectable(outputMode)){
764 output = (OutputModule) outputSelector.select(outputMode);
765 }
766 if (output != null) {
767 output.rollback( null, objectModel, e);
768 } else if (getLogger().isWarnEnabled()) {
769 getLogger().warn("Could not select output mode " + outputMode);
770 }
771 } catch (ServiceException e2) {
772 if (getLogger().isWarnEnabled()) {
773 getLogger().warn("Could not select output mode " + outputMode + ":" + e2.getMessage());
774 }
775 } finally {
776 if (outputSelector != null) {
777 if (output != null) {
778 outputSelector.release(output);
779 }
780 this.manager.release(outputSelector);
781 }
782 }
783 } catch (SQLException se) {
784 if (getLogger().isDebugEnabled())
785 getLogger().debug("There was an error rolling back the transaction", se);
786 }
787 }
788
789 //throw new ProcessingException("Could not add record :position = " + currentIndex, e);
790
791 // don't throw an exception, an error has been signalled, that should suffice
792
793 String throwException = (String) this.settings.get( "throw-exception",
794 param.getParameter( "throw-exception", null ) );
795 if ( throwException != null && BooleanUtils.toBoolean(throwException)) {
796 throw new ProcessingException("Cannot process the requested SQL statement ",e);
797 }
798 } finally {
799 if (conn != null) {
800 try {
801 conn.close();
802 } catch (SQLException sqe) {
803 getLogger().warn("There was an error closing the datasource", sqe);
804 }
805 }
806
807 if (datasource != null)
808 this.dbselector.release(datasource);
809 }
810 if (results != null) {
811 if (rows>0 || (!failed && !this.failOnEmpty)) {
812 results.put("row-count",new Integer(rows));
813 } else {
814 results = null;
815 }
816 } else {
817 if (rows>0) {
818 results = new HashMap(1);
819 results.put("row-count",new Integer(rows));
820 }
821 }
822
823 return results; // (results == null? results : Collections.unmodifiableMap(results));
824 }
825
826 // ========================================================================
827 // abstract methods
828 // ========================================================================
829
830 /**
831 * set all necessary ?s and execute the query
832 * return number of rows processed
833 *
834 * This method is intended to be overridden by classes that
835 * implement other operations e.g. delete
836 */
837 protected abstract int processRow( Map objectModel, Connection conn, PreparedStatement statement, String outputMode,
838 Configuration table, CacheHelper queryData, Object[][] columnValues,
839 int rowIndex, Map results )
840 throws SQLException, ConfigurationException, Exception;
841
842 /**
843 * determine which mode to use as default mode
844 *
845 * This method is intended to be overridden by classes that
846 * implement other operations e.g. delete
847 */
848 protected abstract String selectMode( boolean isAutoIncrement, Map modes );
849
850 /**
851 * determine whether autoincrement columns should be honoured by
852 * this operation. This is usually snsible only for INSERTs.
853 *
854 * This method is intended to be overridden by classes that
855 * implement other operations e.g. delete
856 */
857 protected abstract boolean honourAutoIncrement();
858
859 /**
860 * Fetch all values for all columns that are needed to do the
861 * database operation.
862 *
863 * This method is intended to be overridden by classes that
864 * implement other operations e.g. delete
865 */
866 abstract Object[][] getColumnValues( Configuration tableConf, CacheHelper queryData, Map objectModel )
867 throws ConfigurationException, ServiceException;
868
869 /**
870 * Get the String representation of the PreparedStatement. This is
871 * mapped to the Configuration object itself, so if it doesn't exist,
872 * it will be created.
873 *
874 * This method is intended to be overridden by classes that
875 * implement other operations e.g. delete
876 *
877 * @param table the table's configuration object
878 * @return the insert query as a string
879 */
880 protected abstract CacheHelper getQuery( Configuration table, Map modeTypes, Map defaultModeNames )
881 throws ConfigurationException, ServiceException;
882
883 }