001: package com.bm.testsuite.dataloader;
002:
003: import java.io.File;
004: import java.io.FileInputStream;
005: import java.io.FileNotFoundException;
006: import java.io.IOException;
007: import java.io.InputStream;
008: import java.net.URL;
009: import java.sql.Connection;
010: import java.sql.PreparedStatement;
011: import java.sql.SQLException;
012: import java.sql.Types;
013: import java.text.ParseException;
014: import java.util.ArrayList;
015: import java.util.Arrays;
016: import java.util.Date;
017: import java.util.List;
018: import java.util.Set;
019:
020: import javax.persistence.EntityManager;
021: import javax.persistence.EntityTransaction;
022: import javax.persistence.Query;
023:
024: import com.bm.cfg.Ejb3UnitCfg;
025: import com.bm.introspectors.EmbeddedClassIntrospector;
026: import com.bm.introspectors.EntityBeanIntrospector;
027: import com.bm.introspectors.PersistentPropertyInfo;
028: import com.bm.introspectors.Property;
029: import com.bm.introspectors.relations.EntityReleationInfo;
030: import com.bm.utils.BasicDataSource;
031: import com.bm.utils.Ejb3Utils;
032: import com.bm.utils.SQLUtils;
033: import com.bm.utils.csv.CSVParser;
034:
035: /**
036: * This class creates initial data from a comma separated file.
037: *
038: * @param <T>
039: * the type of the entity bean (mapping the table)
040: *
041: * @author Daniel Wiese
042: * @author Istvan Devai
043: * @author Peter Doornbosch
044: * @since 17.04.2006
045: */
046: public class CSVInitialDataSet<T> implements InitialDataSet {
047:
048: private static final org.apache.log4j.Logger log = org.apache.log4j.Logger
049: .getLogger(CSVInitialDataSet.class);
050:
051: private EntityBeanIntrospector<T> introspector;
052:
053: private String[] propertyMapping;
054:
055: private Property[] propertyInfo;
056:
057: private String insertSQLString;
058:
059: private final File file;
060:
061: private final boolean isCopressed;
062:
063: private List<DateFormats> userDefinedDateFormats = new ArrayList<DateFormats>();
064:
065: private final boolean useSchemaName;
066:
067: /**
068: * Constructor.
069: *
070: * @param entityBeanClass -
071: * the corresponding entity bean class
072: * @param propertyMapping -
073: * a string array with the meaning the first column of the cvs
074: * file belongs to the property with the name
075: * <code>propertyMapping[0]</code>
076: * @param isCompressed -
077: * true if compressed (zip)
078: * @param useSchemaName
079: * the schema name will be used for sql generation
080: * @param csvFileName -
081: * the name of the csv file
082: */
083: public CSVInitialDataSet(Class<T> entityBeanClass,
084: String csvFileName, boolean isCompressed,
085: boolean useSchemaName, String... propertyMapping) {
086: this .useSchemaName = useSchemaName;
087: this .isCopressed = isCompressed;
088: initialize(entityBeanClass, propertyMapping);
089: final URL tmp = Thread.currentThread().getContextClassLoader()
090: .getResource(csvFileName);
091: if (tmp == null) {
092: throw new IllegalArgumentException(
093: "Canīt find the CVS file named (" + csvFileName
094: + ")");
095: }
096:
097: file = new File(Ejb3Utils.getDecodedFilename(tmp));
098:
099: }
100:
101: /**
102: * Constructor.
103: *
104: * @param entityBeanClass -
105: * the corresponding entity bean class
106: * @param propertyMapping -
107: * a string array with the meaning the first column of the csv
108: * file belongs to the property with the name
109: * <code>propertyMapping[0]</code>
110: * @param isCompressed -
111: * true if compressed (zip)
112: * @param csvFileName -
113: * the name of the csv file
114: */
115: public CSVInitialDataSet(Class<T> entityBeanClass,
116: String csvFileName, boolean isCompressed,
117: String... propertyMapping) {
118: this (entityBeanClass, csvFileName, isCompressed, false,
119: propertyMapping);
120: }
121:
122: /**
123: * Constructor.
124: *
125: * @param entityBeanClass -
126: * the corresponding entity bean class
127: * @param propertyMapping -
128: * a string array with the meaning the first column of the csv
129: * file belongs to the property with the name
130: * <code>propertyMapping[0]</code>
131: * @param csvFileName -
132: * the name of the csv file
133: */
134: public CSVInitialDataSet(Class<T> entityBeanClass,
135: String csvFileName, String... propertyMapping) {
136: this (entityBeanClass, csvFileName, false, false,
137: propertyMapping);
138: }
139:
140: /**
141: * Allows to specify a user specific date format pattern's. The are
142: * processed in the added order.
143: *
144: * @param dateFormat
145: * the date format pattern
146: * @return this instance for inlining.
147: */
148: public CSVInitialDataSet<T> addDateFormat(DateFormats dateFormat) {
149: this .userDefinedDateFormats.add(dateFormat);
150: return this ;
151: }
152:
153: /**
154: * @author Daniel Wiese
155: * @since 29.06.2006
156: * @param entityBeanClass
157: * @param propertyMapping
158: */
159: private void initialize(Class<T> entityBeanClass,
160: String... propertyMapping) {
161: this .introspector = new EntityBeanIntrospector<T>(
162: entityBeanClass);
163: // init configuration if not yet done
164: if (!Ejb3UnitCfg.isInitialized()) {
165: List<Class<? extends Object>> usedBeans = new ArrayList<Class<? extends Object>>();
166: usedBeans.add(entityBeanClass);
167: Ejb3UnitCfg.addEntytiesToTest(usedBeans);
168: }
169: this .propertyMapping = propertyMapping;
170: this .propertyInfo = new Property[propertyMapping.length];
171: this .insertSQLString = this .buildInsertSQL();
172: }
173:
174: /**
175: * Returns the insert SQL.
176: *
177: * @return the insert SQL
178: */
179: public String buildInsertSQL() {
180: StringBuilder insertSQL = new StringBuilder();
181: StringBuilder questionMarks = new StringBuilder();
182: insertSQL.append("INSERT INTO ").append(getTableName()).append(
183: " (");
184: int counter = -1;
185: for (String stringProperty : this .propertyMapping) {
186: final Property property = this .getProperty(stringProperty);
187: // persistent field info
188: final PersistentPropertyInfo info = this
189: .getPersistentFieldInfo(stringProperty);
190:
191: insertSQL
192: .append((info.getDbName().length() == 0) ? property
193: .getName() : info.getDbName());
194: questionMarks.append("?");
195: counter++;
196: // store the property
197: this .propertyInfo[counter] = property;
198: if (counter + 1 < this .propertyMapping.length) {
199: insertSQL.append(", ");
200: questionMarks.append(", ");
201: }
202: }
203: // If entity uses single table inheritance, insert a discriminator value
204: // (which must be present)
205: if (this .introspector.usesSingleTableInheritance()) {
206: insertSQL.append(", ").append(
207: this .introspector.getDiscriminatorName()).append(
208: ") ").append("VALUES (").append(
209: questionMarks.toString()).append(", ");
210: Class discriminatorType = this .introspector
211: .getDiscriminatorType();
212: if (discriminatorType.equals(Integer.class)) {
213: insertSQL.append(this .introspector
214: .getDiscriminatorValue());
215: } else {
216: insertSQL.append("'").append(
217: this .introspector.getDiscriminatorValue())
218: .append("'");
219: }
220: insertSQL.append(")");
221: } else {
222: // Normal case: just insert the values.
223: insertSQL.append(") ").append("VALUES (").append(
224: questionMarks.toString()).append(")");
225: }
226:
227: return insertSQL.toString();
228: }
229:
230: private String getTableName() {
231: if (useSchemaName && this .introspector.hasSchema()) {
232: return this .introspector.getShemaName() + "."
233: + this .introspector.getTableName();
234: }
235: return this .introspector.getTableName();
236: }
237:
238: private String getClassName() {
239: return this .introspector.getPersistentClassName();
240: }
241:
242: @SuppressWarnings("unchecked")
243: private Property getProperty(String property) {
244: Property info = null;
245: if (this .introspector.hasEmbeddedPKClass()) {
246: final EmbeddedClassIntrospector pkintro = this .introspector
247: .getEmbeddedPKClass();
248: final List<Property> pkFields = pkintro
249: .getPersitentProperties();
250:
251: for (Property current : pkFields) {
252: if (current.getName().equals(property)) {
253: info = current;
254: break;
255: }
256: }
257: }
258: if (info == null) {
259: for (Property current : this .introspector
260: .getPersitentProperties()) {
261: if (current.getName().equals(property)) {
262: info = current;
263: break;
264: }
265: }
266:
267: if (info == null) {
268: throw new IllegalArgumentException("The property ("
269: + property + ") is not a persistent field");
270: }
271: }
272:
273: return info;
274: }
275:
276: @SuppressWarnings("unchecked")
277: private PersistentPropertyInfo getPersistentFieldInfo(
278: String property) {
279: PersistentPropertyInfo info = null;
280: if (this .introspector.hasEmbeddedPKClass()) {
281: final EmbeddedClassIntrospector pkintro = this .introspector
282: .getEmbeddedPKClass();
283: final List<Property> pkFields = pkintro
284: .getPersitentProperties();
285:
286: for (Property current : pkFields) {
287: if (current.getName().equals(property)) {
288: info = pkintro.getPresistentFieldInfo(current);
289: break;
290: }
291: }
292: }
293: if (info == null) {
294: for (Property current : this .introspector
295: .getPersitentProperties()) {
296: if (current.getName().equals(property)) {
297: info = this .introspector
298: .getPresistentFieldInfo(current);
299: break;
300: }
301: }
302:
303: if (info == null) {
304: throw new IllegalArgumentException("The property ("
305: + property + ") is not a persistent field");
306: }
307: }
308:
309: return info;
310: }
311:
312: /**
313: * Creates the data.
314: *
315: * @author Daniel Wiese
316: * @since 17.04.2006
317: * @see com.bm.testsuite.dataloader.InitialDataSet#create()
318: */
319: public void create() {
320: BasicDataSource ds = new BasicDataSource(Ejb3UnitCfg
321: .getConfiguration());
322: Connection con = null;
323: PreparedStatement prep = null;
324: try {
325: con = ds.getConnection();
326: prep = con.prepareStatement(this .insertSQLString);
327: final CSVParser parser = new CSVParser(getCSVInputStream());
328: parser.setCommentStart("#;!");
329: parser.setEscapes("nrtf", "\n\r\t\f");
330: String value;
331: int count = 0;
332: int lastLineNumber = parser.lastLineNumber();
333: while ((value = parser.nextValue()) != null) {
334: if (parser.lastLineNumber() != lastLineNumber) {
335: // we have a new line
336: lastLineNumber = parser.lastLineNumber();
337: count = 0;
338: }
339:
340: // insert only if neccessary (ignore not requiered fields)
341: if (count < this .propertyInfo.length) {
342: this .setPreparedStatement(count + 1, prep,
343: this .propertyInfo[count], value);
344: count++;
345: }
346:
347: // execute sql
348: if (count == this .propertyInfo.length) {
349: prep.execute();
350: }
351:
352: }
353:
354: parser.close();
355: } catch (FileNotFoundException e) {
356: log.error("Data-Loader failing ", e);
357: new RuntimeException(e);
358: } catch (IOException e) {
359: log.error("Data-Loader failing ", e);
360: new RuntimeException(e);
361: } catch (SQLException e) {
362: log.error("Data-Loader failing ", e);
363: new RuntimeException(e);
364: } finally {
365: SQLUtils.cleanup(con, prep);
366: }
367: }
368:
369: private InputStream getCSVInputStream() {
370: InputStream toReturn = null;
371: if (this .isCopressed) {
372: try {
373: toReturn = Ejb3Utils.unjar(new FileInputStream(
374: this .file));
375: if (toReturn == null) {
376: throw new IllegalArgumentException(
377: "The copressed file " + this .file.getName()
378: + " was empty");
379: }
380: } catch (IOException e) {
381: log.error("The file " + this .file.getName()
382: + " could not be accessed", e);
383: throw new IllegalArgumentException("The file "
384: + this .file.getAbsolutePath()
385: + " could not be accessed", e);
386: }
387:
388: } else {
389: try {
390: toReturn = new FileInputStream(this .file);
391: } catch (FileNotFoundException e) {
392: throw new IllegalArgumentException(
393: "The copressed file " + this .file.getName()
394: + "not found");
395: }
396: }
397:
398: return toReturn;
399: }
400:
401: /**
402: * Deletes the data.
403: *
404: * @param em -
405: * the entyty manager.
406: * @author Daniel Wiese
407: * @since 17.04.2006
408: * @see com.bm.testsuite.dataloader.InitialDataSet#cleanup(EntityManager)
409: */
410: public void cleanup(EntityManager em) {
411: StringBuilder deleteSQL = new StringBuilder();
412: deleteSQL.append("DELETE FROM ").append(getClassName());
413: EntityTransaction tx = em.getTransaction();
414: tx.begin();
415: Query query = em.createQuery(deleteSQL.toString());
416: query.executeUpdate();
417: tx.commit();
418:
419: }
420:
421: /**
422: * Sets the value (using the right type) in the prepared statement.
423: *
424: * @param index -
425: * the index inside the prepared statement
426: * @param statement -
427: * the prepared statement itself
428: * @param prop -
429: * the property representing the type
430: * @param value -
431: * the value to be set
432: * @throws SQLException -
433: * in error case
434: */
435: private void setPreparedStatement(int index,
436: PreparedStatement statement, Property prop, String value)
437: throws SQLException {
438:
439: // First, check whether this property denotes a relation
440: PersistentPropertyInfo persistentFieldInfo = getPersistentFieldInfo(prop
441: .getPropertyName());
442: if (persistentFieldInfo.isReleation()) {
443: EntityReleationInfo relationInfo = persistentFieldInfo
444: .getEntityReleationInfo();
445: // Must determine the type of the primary key of the class that is referenced by the relation.
446: Set<Property> targetKeyProps = relationInfo
447: .getTargetKeyProperty();
448: if (targetKeyProps == null) {
449: // This can happen with relation types that we do not yet support
450: throw new IllegalArgumentException(
451: "Can't determine key type of relation target - relation type not yet supported?");
452: }
453: if (targetKeyProps.size() > 1) {
454: throw new IllegalArgumentException(
455: "Composite foreign keys are not yet supported.");
456: }
457: for (Property keyProp : targetKeyProps) { // Because of the check above, this will loop at most once
458: Class foreignKeyType = Ejb3Utils
459: .getNonPrimitiveType(keyProp.getType());
460: setPreparedStatement(index, statement, foreignKeyType,
461: value);
462: }
463: } else {
464: // convert to non-primitive if primitive
465: Class type = Ejb3Utils.getNonPrimitiveType(prop.getType());
466: setPreparedStatement(index, statement, type, value);
467: }
468: }
469:
470: /**
471: * Sets the value (using the right type) in the prepared statement. Supports only simple types
472: * @param index
473: * the index of the value in the prepared statement
474: * @param statement
475: * the prepared statement in which values are set
476: * @param type
477: * the type of the property
478: * @param value
479: * the value to set
480: * @throws SQLException
481: */
482: private void setPreparedStatement(int index,
483: PreparedStatement statement, Class type, String value)
484: throws SQLException {
485: if (type.equals(String.class)) {
486: statement.setString(index, value);
487: } else if (type.equals(Integer.class)) {
488: if (value == null || value.equals("")
489: || value.equals("null")) {
490: statement.setNull(index, Types.INTEGER);
491: } else {
492: statement.setInt(index, ((value.equals("")) ? 0
493: : Integer.valueOf(value)));
494: }
495: } else if (type.equals(Long.class)) {
496: if (value == null || value.equals("")
497: || value.equals("null")) {
498: statement.setNull(index, Types.BIGINT);
499: } else {
500: statement.setLong(index, ((value.equals("")) ? 0 : Long
501: .valueOf(value)));
502: }
503: } else if (type.equals(Boolean.class)) {
504: final boolean result = (value != null
505: && value.equals("True") ? true : false);
506: statement.setBoolean(index, result);
507: } else if (type.equals(Short.class)) {
508: statement.setShort(index, ((value.equals("")) ? 0 : Short
509: .valueOf(value)));
510: } else if (type.equals(Byte.class)) {
511: statement.setByte(index, Byte.valueOf(value));
512: } else if (type.equals(Character.class)) {
513: statement.setString(index, String.valueOf(value));
514: } else if (type.equals(Date.class)) {
515: parseAndSetDate(index, value, statement);
516: } else if (type.equals(Double.class)) {
517: statement.setDouble(index, ((value.equals("")) ? 0 : Double
518: .valueOf(value)));
519: } else if (type.equals(Float.class)) {
520: statement.setFloat(index, ((value.equals("")) ? 0 : Float
521: .valueOf(value)));
522: } else if (type.isEnum()) {
523: // TODO: possible to have the ordinal value of an
524: // enum in a .csv file maybe it would be reasonable to extend this
525: // so that it is also possible to have enums by literal name
526: statement.setInt(index, Integer.valueOf(value));
527: }
528: }
529:
530: private void parseAndSetDate(int index, String value,
531: PreparedStatement statement) throws SQLException {
532: if (value.equals("") || value.equalsIgnoreCase("null")) {
533: statement.setNull(index, java.sql.Types.DATE);
534: } else {
535: // try to guess the default format
536: boolean success = false;
537: List<DateFormats> allValidFormats = getValidFormats();
538: for (DateFormats current : allValidFormats) {
539: try {
540: current.parseToPreparedStatemnt(value, statement,
541: index);
542: success = true;
543: break;
544: } catch (ParseException ex) {
545: log.debug("Date parser (" + current
546: + ") was not working, trying next");
547: }
548: }
549:
550: if (!success) {
551: // java 1.5 will convert this to string builder internally
552: String msg = "Illegal date format (" + value + ")";
553: msg += " expecting one of: ";
554: for (DateFormats current : DateFormats.values()) {
555: msg += current.toPattern() + ", ";
556: }
557:
558: throw new IllegalArgumentException(msg);
559: }
560: }
561: }
562:
563: private List<DateFormats> getValidFormats() {
564: final List<DateFormats> dtFormats = new ArrayList<DateFormats>();
565: if (!this.userDefinedDateFormats.isEmpty()) {
566: dtFormats.addAll(this.userDefinedDateFormats);
567: }
568:
569: dtFormats.addAll(Arrays.asList(DateFormats.systemValues()));
570: return dtFormats;
571: }
572:
573: }
|