blob: 12a8fcd7cd2fc9fcb18df532af6e2dbe5647c9eb [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cocoon.acting;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import org.apache.avalon.excalibur.datasource.DataSourceComponent;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.parameters.Parameters;
import org.apache.avalon.framework.thread.ThreadSafe;
import org.apache.cocoon.Constants;
import org.apache.cocoon.ProcessingException;
import org.apache.cocoon.environment.ObjectModelHelper;
import org.apache.cocoon.environment.Redirector;
import org.apache.cocoon.environment.Request;
import org.apache.cocoon.environment.SourceResolver;
import org.apache.commons.lang.StringUtils;
/**
* Adds record in a database. The action can update one or more tables,
* and can add more than one row to a table at a time. The form descriptor
* semantics for this are still in a bit of a state of flux. Note
* that if a secondary table relies on the value of a new primary key in a
* primary table, the primary key must be created using manual mode.
*
* @author <a href="mailto:bloritsch@apache.org">Berin Loritsch</a>
* @author <a href="mailto:balld@apache.org">Donald Ball</a>
* @version CVS $Id$
*/
public class DatabaseAddAction extends AbstractDatabaseAction implements ThreadSafe {
protected static final Map addStatements = new HashMap();
private static final Map selectStatements = new HashMap();
/**
* Add a record to the database. This action assumes that
* the file referenced by the "descriptor" parameter conforms
* to the AbstractDatabaseAction specifications.
*/
public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters param) throws Exception {
DataSourceComponent datasource = null;
Connection conn = null;
Map results = new HashMap();
// read global parameter settings
boolean reloadable = Constants.DESCRIPTOR_RELOADABLE_DEFAULT;
if (this.settings.containsKey("reloadable"))
reloadable = Boolean.valueOf((String) this.settings.get("reloadable")).booleanValue();
// read local parameter settings
try {
Configuration conf =
this.getConfiguration(param.getParameter("descriptor", (String) this.settings.get("descriptor")), resolver,
param.getParameterAsBoolean("reloadable",reloadable));
datasource = this.getDataSource(conf);
conn = datasource.getConnection();
Request request = ObjectModelHelper.getRequest(objectModel);
if (conn.getAutoCommit()) {
conn.setAutoCommit(false);
}
Configuration[] tables = conf.getChildren("table");
for (int i=0; i<tables.length; i++) {
Configuration table = tables[i];
processTable(table,conn,request,results);
}
conn.commit();
} catch (Exception e) {
if (conn != null) {
try {
conn.rollback();
} catch (SQLException se) {
getLogger().debug("There was an error rolling back the transaction", se);
}
}
//throw new ProcessingException("Could not add record :position = " + currentIndex, e);
throw new ProcessingException("Could not add record",e);
} finally {
if (conn != null) {
try {
conn.close();
} catch (SQLException sqe) {
getLogger().warn("There was an error closing the datasource", sqe);
}
}
if (datasource != null) this.dbselector.release(datasource);
}
return Collections.unmodifiableMap(results);
}
/**
* Inserts a row or a set of rows into the given table based on the
* request parameters
*
* @param table the table's configuration
* @param conn the database connection
* @param request the request
*/
void processTable(Configuration table, Connection conn, Request request, Map results) throws SQLException,ConfigurationException,Exception {
PreparedStatement statement = null;
try {
String query = this.getAddQuery(table);
getLogger().debug("Add query: "+query);
statement = conn.prepareStatement(query);
Configuration[] keys = table.getChild("keys").getChildren("key");
Configuration[] values = table.getChild("values").getChildren("value");
int currentIndex = 1;
boolean manyrows = false;
int wildcardIndex = -1;
String wildcardParam = null;
for (int i=0; i<keys.length; i++) {
wildcardParam = keys[i].getAttribute("param");
if ((wildcardIndex = wildcardParam.indexOf('*')) != -1) {
manyrows = true;
break;
}
}
if (manyrows) {
/**
* This table has a column with a wildcard, so we're going
* to be inserting n rows, where 0 <= n
*/
String prefix = wildcardParam.substring(0,wildcardIndex);
String suffix = StringUtils.substring(wildcardParam, wildcardIndex + 1);
Enumeration names = request.getParameterNames();
SortedSet matchset = new TreeSet();
int prefixLength = prefix.length();
int length = prefixLength + suffix.length();
while (names.hasMoreElements()) {
String name = (String)names.nextElement();
if (name.startsWith(prefix) && name.endsWith(suffix)) {
String wildcard = StringUtils.mid(name, prefixLength, name.length() - length);
matchset.add(wildcard);
}
}
int rowIndex = 1;
Iterator iterator = matchset.iterator();
while (iterator.hasNext()) {
String wildcard = (String)iterator.next();
currentIndex = 1;
for (int j=0; j<keys.length; j++) {
String myparam = getActualParam(keys[j].getAttribute("param"),wildcard);
currentIndex += setKey(table,keys[j],conn,statement,currentIndex,request,myparam,results);
}
for (int j=0; j<values.length; j++) {
String myparam = getActualParam(values[j].getAttribute("param"),wildcard);
this.setColumn(statement,currentIndex,request,values[j],myparam,request.getParameter(myparam),rowIndex);
currentIndex++;
}
statement.execute();
rowIndex++;
}
} else {
/**
* This table has no wildcard columns, so we're going to
* be inserting 1 row.
*/
for (int i = 0; i < keys.length; i++) {
currentIndex += setKey(table,keys[i],conn,statement,currentIndex,request,keys[i].getAttribute("param",""),results);
}
for (int i = 0; i < values.length; i++, currentIndex++) {
this.setColumn(statement, currentIndex, request, values[i]);
}
statement.execute();
/** Done processing table **/
}
} finally {
try {
if (statement != null) {
statement.close();
}
} catch (SQLException e) {}
}
}
/**
* Sets the key value on the prepared statement. There are four modes:
*
* <dl>
* <dt>automatic (default)</dt>
* <dd>let the database automatically create the key. note this
* prohibits the action from storing the key value anywhere.</dd>
* <dt>manual</dt>
* <dd>create the key value using SELECT(dbcol)+1 from TABLE</dd>
* <dt>form</dt>
* <dd>look for the key value in the request parameters</dd>
* <dt>request-attribute</dt>
* <dd>look for the key value in the request attributes</dd>
* </dl>
*
* This method has a couple of side effects. If the mode is manual,
* the key value is stored in the request object's attributes for use
* by other inserts. The key is the string "key:TABLENAME:DBCOL".
* This method also puts the value of manually created keys in the results
* map. That key is simply the value of the dbcol attribute. Note this
* stuff is definitely up in the air.
*
* @param table the table's configuration object
* @param key the key's configuration object
* @param conn the database connection
* @param statement the insert statement
* @param currentIndex the position of the key column
* @param request the request object
* @param param the actual name of the request parameter
* @return the number of columns by which to increment the currentIndex
*/
int setKey(Configuration table, Configuration key, Connection conn, PreparedStatement statement, int currentIndex, Request request, String param, Map results) throws ConfigurationException, SQLException,Exception {
String mode = key.getAttribute("mode","automatic");
String keyname = new StringBuffer("key:").append(table.getAttribute("name"))
.append(':').append(key.getAttribute("dbcol")).toString();
if ("manual".equals(mode)) {
// Set the key value using SELECT MAX(keyname)+1
String selectQuery = this.getSelectQuery(key);
PreparedStatement select_statement = conn.prepareStatement(selectQuery);
ResultSet set = select_statement.executeQuery();
set.next();
int value = set.getInt("maxid") + 1;
statement.setInt(currentIndex, value);
getLogger().debug("Manually setting key to " + value);
setRequestAttribute(request,keyname,new Integer(value));
results.put(key.getAttribute("dbcol"),String.valueOf(value));
set.close();
select_statement.close();
} else if ("form".equals(mode)) {
// Set the key value from the request
getLogger().debug("Setting key from form");
this.setColumn(statement, currentIndex, request, key, param);
} else if ("request-attribute".equals(mode)) {
Integer value = (Integer)getRequestAttribute(request,key.getAttribute("request-attribute-name"));
getLogger().debug("Setting key from request attribute "+value);
statement.setInt(currentIndex,value.intValue());
} else {
getLogger().debug("Automatically setting key");
// The database automatically creates a key value
return 0;
}
return 1;
}
/**
* Returns the actual name of the parameter. If the name contains
* no wildcard, the param is returned untouched, otherwise the
* wildcard value is substituted for the * character. This probably
* doesn't deserve a method unto itself, but I can imagine wanting
* to use a more sophisticated matching and substitution algorithm.
*
* @param param the name of the parameter, possibly with a wildcard char
* @param wildcard the wildcard value
* @return the actual name of the parameter
*/
String getActualParam(String param, String wildcard) {
int index;
if ((index = param.indexOf('*')) != -1) {
return param.substring(0,index)+wildcard+param.substring(index+1);
} else {
return param;
}
}
/**
* Get the String representation of the PreparedStatement. This is
* mapped to the Configuration object itself, so if it doesn't exist,
* it will be created.
*
* @param table the table's configuration object
* @return the insert query as a string
*/
protected String getAddQuery(Configuration table) throws ConfigurationException {
String query = null;
synchronized (DatabaseAddAction.addStatements) {
query = (String) DatabaseAddAction.addStatements.get(table);
if (query == null) {
Configuration[] values = table.getChild("values").getChildren("value");
Configuration[] keys = table.getChild("keys").getChildren("key");
StringBuffer queryBuffer = new StringBuffer("INSERT INTO ");
queryBuffer.append(table.getAttribute("name"));
queryBuffer.append(" (");
int numParams = 0;
for (int i = 0; i < keys.length; i++) {
String mode = keys[i].getAttribute("mode", "automatic");
if ("manual".equals(mode) || "form".equals(mode) || "request-attribute".equals(mode)) {
if (numParams > 0) {
queryBuffer.append(", ");
}
queryBuffer.append(keys[i].getAttribute("dbcol"));
this.setSelectQuery(table.getAttribute("name"), keys[i]);
numParams++;
}
}
queryBuffer.append(buildList(values, numParams));
numParams += values.length;
queryBuffer.append(") VALUES (");
if (numParams > 0) {
queryBuffer.append("?");
queryBuffer.append(StringUtils.repeat(", ?", numParams - 1));
}
queryBuffer.append(")");
query = queryBuffer.toString();
DatabaseAddAction.addStatements.put(table, query);
}
}
return query;
}
/**
* Set the String representation of the MaxID lookup statement. This is
* mapped to the Configuration object itself, so if it doesn't exist,
* it will be created.
*/
protected final synchronized void setSelectQuery(String tableName, Configuration entry) throws ConfigurationException {
StringBuffer queryBuffer = new StringBuffer("SELECT max(");
queryBuffer.append(entry.getAttribute("dbcol"));
queryBuffer.append(") AS maxid FROM ");
queryBuffer.append(tableName);
DatabaseAddAction.selectStatements.put(entry, queryBuffer.toString());
}
protected final synchronized String getSelectQuery(Configuration entry) throws ConfigurationException {
return (String) DatabaseAddAction.selectStatements.get(entry);
}
}