001: package dinamica;
002:
003: import java.util.HashMap;
004: import javax.naming.*;
005: import javax.servlet.http.HttpSession;
006: import javax.sql.DataSource;
007:
008: /**
009: * Base class to program business transaction services (read/write).
010: * All transactions will subclass this class.
011: *
012: * <br>
013: * Creation date: 4/10/2003<br>
014: * Last Update: 4/10/2003<br>
015: * (c) 2003 Martin Cordova<br>
016: * This code is released under the LGPL license<br>
017: * @author Martin Cordova
018: */
019: public class GenericTransaction extends AbstractModule {
020:
021: /** store recordsets published by this service */
022: private HashMap<String, Recordset> _publish = new HashMap<String, Recordset>();
023:
024: /**
025: * Publish recordset to be consumed by Output modules
026: * @param key Recordset ID
027: * @param data Recordset object
028: */
029: protected void publish(String key, Recordset data) throws Throwable {
030: _publish.put(key, data);
031:
032: /* get recordset simple metadata (recordcount, pagecount, etc) */
033: data.setID(key);
034: Recordset info = data.getRecordsetInfo();
035:
036: /* publish this new recordset */
037: String infoID = key + ".metadata";
038: _publish.put(infoID, info);
039:
040: }
041:
042: /**
043: * Transaction service - this method must be redefined
044: * by descendants of this class, include a super.service(inputParams)
045: * as the first line of your service() method code to reuse base
046: * functionality (auto-creation of recordsets based on recordset elements defined in config.xml).
047: * In this method the business logic will be contained, and results will be represented
048: * as recordsets that will be consumed by Output objects. Recordsets are published using
049: * the method publish(id, rsObject). This class provides a method to retrieve
050: * a recordset using its ID and throws an error if the recordset is not present in the HashMap.<br>
051: * If inputParams is not null then it is published with the id "_request".
052: * @param inputParams Request parameters pre-validated and represented as a Recordset with one record.
053: * Recordset fields are set according to the data types defined in the validator.xml file.
054: * @return 0 if success - any other return values are user defined
055: * @throws Throwable
056: */
057: public int service(Recordset inputParams) throws Throwable {
058: int rc = createRecordsets(inputParams);
059:
060: if (inputParams != null)
061: _publish.put("_request", inputParams);
062:
063: return rc;
064:
065: }
066:
067: /**
068: * Create recordsets using config.xml parameters.
069: * For recordsets created using SQL templates, all values
070: * from the inputParams recordset will be auto-replaced into
071: * the template. This recordset is only created when using
072: * a validator (validator.xml definition to auto-validate request parameters).
073: * @throws Throwable in case of invalid config.xml parameters or JDBC exceptions.
074: */
075: protected int createRecordsets(Recordset inputParams)
076: throws Throwable {
077:
078: int rc = 0;
079:
080: /* get database object */
081: Db db = getDb();
082:
083: /* get recordsets config */
084: Recordset rs = _config.getRecordsets();
085: Recordset rs1 = null;
086:
087: /* for each defined recordset */
088: while (rs.next()) {
089:
090: /* get parameters */
091: String id = (String) rs.getValue("id");
092: String source = (String) rs.getValue("source");
093: String scope = (String) rs.getValue("scope");
094: String onempty = (String) rs.getValue("onempty");
095: String maxRows = (String) rs.getValue("maxrows");
096: int limit = 0;
097: if (maxRows != null)
098: limit = Integer.parseInt(maxRows);
099: String dataSrc = rs.getString("datasource");
100: String params = rs.getString("params");
101:
102: /* create recordset using appropiate source */
103: if (source.equals("sql")) {
104: String sqlFile = getResource(id);
105: sqlFile = this .getSQL(sqlFile, inputParams);
106:
107: if (params != null) {
108: // PATCH 2005-04-05 support for alternative input parameters recordset for SQL templates
109: Recordset rsParams = getRecordset(params);
110: if (rsParams.getRecordCount() > 0)
111: rsParams.first();
112: else
113: throw new Throwable(
114: "The recordset ["
115: + params
116: + "] used to replace SQL template values is empty.");
117: sqlFile = this .getSQL(sqlFile, rsParams);
118: }
119:
120: //PATCH 2005-03-14 support datasource defined at recordset level
121: if (dataSrc == null) {
122: if (limit > 0)
123: rs1 = db.get(sqlFile, limit);
124: else
125: rs1 = db.get(sqlFile);
126: } else {
127: rs1 = dbGet(dataSrc, sqlFile, limit);
128: }
129:
130: if (onempty != null) {
131: if (rs1.getRecordCount() == 0)
132: rc = Integer.parseInt(onempty);
133: }
134: } else if (source.equals("session")) {
135: rs1 = (Recordset) getSession().getAttribute(id);
136: //PATCH 2005-03-01 - enhance error message if session attribute is null
137: if (rs1 == null)
138: throw new Throwable(
139: "Recordset ["
140: + id
141: + "] not found in Session attribute, maybe the application was reloaded, destroying the session.");
142: } else if (source.equals("request")) {
143: rs1 = (Recordset) _req.getAttribute(id);
144: if (rs1 == null)
145: throw new Throwable("Request attribute [" + id
146: + "] does not contain a recordset.");
147: } else if (source.equals("textfile")) {
148: rs1 = this .getRsFromFlatFile(id);
149: } else if (source.equals("class")) {
150: IRecordsetProvider rsProv = (IRecordsetProvider) getObject(id);
151: rs1 = rsProv.getRecordset(inputParams);
152: } else {
153: throw new Throwable(
154: "Invalid recordset source in config.xml ("
155: + _config.path
156: + "). Source attribute values can be sql, session, textfile or request only.");
157: }
158:
159: /* publish this recordset */
160: _publish.put(id, rs1);
161:
162: /* get recordset simple metadata (recordcount, pagecount, etc) */
163: rs1.setID(id);
164: Recordset info = rs1.getRecordsetInfo();
165:
166: /* publish this new recordset */
167: String infoID = id + ".metadata";
168: _publish.put(infoID, info);
169:
170: /* persist recordset if necessary (in session or request object */
171: if (scope.equals("session")) {
172: getSession().setAttribute(id, rs1);
173: } else if (scope.equals("request")) {
174: _req.setAttribute(id, rs1);
175: } else if (!scope.equals("transaction")) {
176: throw new Throwable(
177: "Invalid recordset scope in config.xml ("
178: + _config.path
179: + "). Scope attribute values can be transaction, session or request only.");
180: }
181:
182: }
183:
184: return rc;
185: }
186:
187: /**
188: * Returns a recordset published by this transaction
189: * @param id ID or symbolic name which was used to publish the
190: * recordset - either by code or using the config.xml elements.
191: * @return Recordset
192: * @throws Throwable if ID oes not match any of the IDs of the published recordsets
193: */
194: public Recordset getRecordset(String id) throws Throwable {
195: if (_publish.containsKey(id)) {
196: return (Recordset) _publish.get(id);
197: } else {
198: throw new Throwable("Invalid recordset ID: " + id);
199: }
200: }
201:
202: /**
203: * Generate SQL command. Encapsulates the use of the TemplateEngine
204: * class, to make it easier for developers writing Transaction Modules
205: * @param sql SQL Template
206: * @param rs Recordset with at least one record - there must be
207: * a current record
208: * @return SQL command with replaced values
209: * @throws Throwable
210: */
211: protected String getSQL(String sql, Recordset rs) throws Throwable {
212:
213: TemplateEngine t = new TemplateEngine(_ctx, _req, sql);
214:
215: //patch 2007-10-25
216: //use custom locale for LABELS in SQL template
217: HttpSession s = getRequest().getSession(true);
218: java.util.Locale l = (java.util.Locale) s
219: .getAttribute("dinamica.user.locale");
220: t.setLocale(l);
221:
222: return t.getSql(rs);
223:
224: }
225:
226: /**
227: * Load the appropiate class and creates an object
228: * that MUST subclass GenericTransaction. This method is
229: * used by Transactions that delegate work on "subtransaction"
230: * objects. All these classes subclass GenericTransaction to inherit all the
231: * code supporting business logic programming. You may define your
232: * own methods in those classes, they are intended to refactor
233: * common business code that may be used by multiple Transactions.<br>
234: * Typically, you will use code like this:<br>
235: * <pre>
236: * MyOwnClass obj = (MyOwnClass)getObject("mypackage.MyOwnClass");
237: * obj.myMethod();
238: * </pre>
239: * <br>
240: * An object created this way inherits all the power of
241: * the GenericTransaction, including the availability of
242: * security information (current user), access to the same
243: * database connection as the caller, etc. Both objects participate
244: * in the same JDBC Transaction if this feature was enabled in
245: * the config.xml file.
246: *
247: * @param className Name of the class to instantiate
248: * @return An object of class GenericTransaction
249: * @throws Throwable
250: */
251: protected GenericTransaction getObject(String className)
252: throws Throwable {
253:
254: GenericTransaction t = null;
255:
256: /* load transaction class */
257: t = (GenericTransaction) Thread.currentThread()
258: .getContextClassLoader().loadClass(className)
259: .newInstance();
260: t.init(_ctx, _req, _res);
261: t.setConfig(_config);
262: t.setConnection(_conn);
263:
264: /* log jdbc performance? */
265: t.setLogWriter(_pw);
266:
267: return t;
268:
269: }
270:
271: /**
272: * Create a recordset with all the fields
273: * required to produce a chart with ChartOutput. This recordset
274: * will contain no records.
275: * @return Recordset with the column structure required by
276: * the class ChartOutput
277: * @throws Throwable
278: */
279: public Recordset getChartInfoRecordset() throws Throwable {
280: /* define chart params recordset */
281: Recordset rs = new Recordset();
282: rs.append("chart-plugin", java.sql.Types.VARCHAR);
283: rs.append("title", java.sql.Types.VARCHAR);
284: rs.append("title-x", java.sql.Types.VARCHAR);
285: rs.append("title-y", java.sql.Types.VARCHAR);
286: rs.append("column-x", java.sql.Types.VARCHAR);
287: rs.append("column-y", java.sql.Types.VARCHAR);
288: rs.append("title-series", java.sql.Types.VARCHAR);
289: rs.append("width", java.sql.Types.INTEGER);
290: rs.append("height", java.sql.Types.INTEGER);
291: rs.append("data", java.sql.Types.VARCHAR);
292: rs.append("dateformat", java.sql.Types.VARCHAR);
293:
294: //added on april-06-2004
295: rs.append("session", java.sql.Types.VARCHAR); //true|false: save in session?
296: rs.append("image-id", java.sql.Types.VARCHAR);//session attribute id
297:
298: //added on july-19-2005
299: rs.append("color", java.sql.Types.VARCHAR); //true|false: save in session?
300:
301: return rs;
302: }
303:
304: /**
305: * Return DataSource object using JNDI prefix
306: * configured in web.xml context parameter. This is an
307: * utility method to help simplify Transaction code. A DataSource
308: * can be obtained with a single line of code:<br><br>
309: * <pre>
310: * javax.sql.DataSource ds = getDataSource("jdbc/customersDB");
311: * setConnection(ds.getConnection);
312: * ....
313: * </pre>
314: * <br>
315: * Remember that when you use your own datasource, you
316: * must close the connection in your Transaction code! consult
317: * the reference guide ("Sample code" section) for more information.
318: *
319: * @param name Name of the datasource (Example: jdbc/customersdb)
320: * @return DataSource object
321: * @throws Throwable If DataSource cannot be obtained
322: */
323: protected DataSource getDataSource(String name) throws Throwable {
324:
325: //get datasource config from web.xml
326: String jndiPrefix = "";
327: if (getContext() != null)
328: jndiPrefix = getContext().getInitParameter("jndi-prefix");
329: else
330: jndiPrefix = "java:comp/env/";
331:
332: if (jndiPrefix == null)
333: jndiPrefix = "";
334:
335: DataSource ds = Jndi.getDataSource(jndiPrefix + name);
336: if (ds == null)
337: throw new Throwable("Can't get datasource: " + name);
338:
339: return ds;
340:
341: }
342:
343: /**
344: * Return the default application DataSource object
345: * as configured in web.xml context parameters. This is a
346: * utility method to help simplify Transaction code. A DataSource
347: * can be obtained with a single line of code:<br><br>
348: * <pre>
349: * javax.sql.DataSource ds = getDataSource();
350: * setConnection(ds.getConnection());
351: * ....
352: * </pre>
353: * <br>
354: * Remember that when you use your own datasource, you
355: * must close the connection in your Transaction code! please consult
356: * the reference guide ("Sample code" section) for more information.
357: *
358: * @return DataSource object
359: * @throws Throwable If DataSource cannot be obtained
360: */
361: protected DataSource getDataSource() throws Throwable {
362:
363: //get datasource config from web.xml
364: String jndiPrefix = null;
365: String name = null;
366:
367: if (getContext() != null) {
368:
369: if (getConfig().transDataSource != null)
370: name = getConfig().transDataSource;
371: else
372: name = getContext().getInitParameter("def-datasource");
373:
374: jndiPrefix = getContext().getInitParameter("jndi-prefix");
375:
376: } else
377: throw new Throwable(
378: "This method can't return a datasource if servlet the context is null.");
379:
380: if (jndiPrefix == null)
381: jndiPrefix = "";
382:
383: DataSource ds = Jndi.getDataSource(jndiPrefix + name);
384: if (ds == null)
385: throw new Throwable("Can't get datasource: " + name);
386:
387: return ds;
388:
389: }
390:
391: /**
392: * Returns an "env-entry" value stored in web.xml.
393: * @param name env-entry-name element
394: **/
395: protected String getEnvEntry(String name) throws Throwable {
396:
397: Context env = (Context) new InitialContext()
398: .lookup("java:comp/env");
399: String v = (String) env.lookup(name);
400: return v;
401:
402: }
403:
404: /**
405: * Retrieve internal HashMap containing all published Recordsets
406: * in case some output module needs to serialize this object
407: * or anything else
408: * @return HashMap containing all published Recordsets
409: */
410: public HashMap<String, Recordset> getData() {
411: return _publish;
412: }
413:
414: /**
415: * Utility method to retrieve a recordset from a different data source
416: * than the one used by the action
417: * @param DataSourceName Data Source name like "jdbc/xxxx"
418: * @param sql SQL command that returns a result set
419: * @param limit The maximum number of rows to retrieve (0 = no limit)
420: * @return
421: * @throws Throwable
422: */
423: protected Recordset dbGet(String DataSourceName, String sql,
424: int limit) throws Throwable {
425: java.sql.Connection conn = getDataSource(DataSourceName)
426: .getConnection();
427: try {
428: Db db = new Db(conn);
429:
430: if (this ._pw != null)
431: db.setLogWriter(_pw);
432:
433: if (limit > 0)
434: return db.get(sql, limit);
435: else
436: return db.get(sql);
437: } catch (Throwable e) {
438: throw e;
439: } finally {
440: if (conn != null)
441: conn.close();
442: }
443: }
444:
445: /**
446: * Utility method to retrieve a recordset from a different data source
447: * than the one used by the action
448: * @param DataSourceName Data Source name like "jdbc/xxxx"
449: * @param sql SQL command that returns a result set
450: * @return
451: * @throws Throwable
452: */
453: protected Recordset dbGet(String DataSourceName, String sql)
454: throws Throwable {
455: return dbGet(DataSourceName, sql, 0);
456: }
457:
458: /**
459: * Creates a recordset according to a structure defined in a
460: * flat file. The 1st line defines the column types, the second line
461: * defines the column names. From 3rd line begins data records. Columns
462: * are separated by TAB, rows are separated by CR+NL.
463: * @param path Path to flat file defining recordset structure and data. If path starts with "/..." it is interpreted as a location relative
464: * to the context, otherwise it is assumed to be located in the Action's path.
465: * @return Recordset according to the flat file structure
466: * @throws Throwable
467: */
468: protected Recordset getRsFromFlatFile(String path) throws Throwable {
469: Recordset rs = new Recordset();
470: String data = getResource(path);
471: String lineSep = "\r\n";
472: if (data.indexOf(lineSep) < 0)
473: lineSep = "\n";
474:
475: String rows[] = StringUtil.split(data, lineSep);
476: String listseparator = "\t";
477:
478: //adjust which list separator is used, tab or comma or semicolon
479: if (rows[0].indexOf(",") != -1)
480: listseparator = ",";
481: else if (rows[0].indexOf(";") != -1)
482: listseparator = ";";
483:
484: String fields[] = StringUtil.split(rows[0], listseparator);
485: String names[] = StringUtil.split(rows[1], listseparator);
486: boolean is_blank_row = false;
487:
488: if (fields.length != names.length)
489: throw new Throwable(
490: "Row #2 (column names) does not match the right number of columns.");
491:
492: for (int i = 0; i < fields.length; i++) {
493: if (fields[i].toLowerCase().equals("varchar"))
494: rs.append(names[i], java.sql.Types.VARCHAR);
495: else if (fields[i].toLowerCase().equals("date"))
496: rs.append(names[i], java.sql.Types.DATE);
497: else if (fields[i].toLowerCase().equals("integer"))
498: rs.append(names[i], java.sql.Types.INTEGER);
499: else if (fields[i].toLowerCase().equals("double"))
500: rs.append(names[i], java.sql.Types.DOUBLE);
501: else {
502: throw new Throwable(
503: "Invalid column type ["
504: + fields[i]
505: + "]. Valid column types are: varchar, date, integer and double.");
506: }
507:
508: }
509:
510: for (int i = 2; i < rows.length; i++) {
511: //here is a empty line
512: if (rows[i].equals(""))
513: continue;
514:
515: //add a record if not all of last row is null,flatfile recordset does not allow all fields is null
516: if (!is_blank_row)
517: rs.addNew();
518:
519: //initial flag
520: is_blank_row = true;
521:
522: String value[] = StringUtil.split(rows[i], listseparator);
523:
524: //if (fields.length!=value.length)
525: // throw new Throwable("Row #" + i + " does not match the right number of columns.");
526:
527: for (int j = 0; j < Math.min(fields.length, value.length); j++) {
528: //replace lables such as ${lbl:},${ses:},%{req:}
529: value[j] = getSQL(value[j], null);
530:
531: //if this field is null,then set field null.if all fields is null,is_blank_row will equal true.
532: if (value[j].equals("")
533: || value[j].toLowerCase().equals("null")) {
534: rs.setValue(names[j], null);
535: } else {
536: is_blank_row = false;
537: if (fields[j].toLowerCase().equals("varchar"))
538: rs.setValue(names[j], value[j]);
539: else if (fields[j].toLowerCase().equals("date")) {
540: if (value[j].indexOf("@") != -1) //formated date value
541: {
542: String date[] = StringUtil.split(value[j],
543: "@");
544: rs.setValue(names[j], StringUtil
545: .getDateObject(date[0], date[1]));
546: } else {
547: rs.setValue(names[j], StringUtil
548: .getDateObject(value[j],
549: "yyyy-MM-dd"));
550: }
551: } else if (fields[j].toLowerCase()
552: .equals("integer"))
553: rs.setValue(names[j], new Integer(value[j]));
554: else if (fields[j].toLowerCase().equals("double"))
555: rs.setValue(names[j], new Double(value[j]));
556: }
557: }
558: }
559:
560: //remove the last row if all field is null
561: if (is_blank_row)
562: rs.delete(rs.getRecordNumber());
563:
564: return rs;
565: }
566:
567: }
|