0001: /*
0002: * Copyright 2003 The Apache Software Foundation.
0003: *
0004: * Licensed under the Apache License, Version 2.0 (the "License");
0005: * you may not use this file except in compliance with the License.
0006: * You may obtain a copy of the License at
0007: *
0008: * http://www.apache.org/licenses/LICENSE-2.0
0009: *
0010: * Unless required by applicable law or agreed to in writing, software
0011: * distributed under the License is distributed on an "AS IS" BASIS,
0012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0013: * See the License for the specific language governing permissions and
0014: * limitations under the License.
0015: */
0016:
0017: package velosurf.model;
0018:
0019: import java.lang.reflect.Constructor;
0020: import java.sql.SQLException;
0021: import java.sql.Types;
0022: import java.util.*;
0023:
0024: import velosurf.cache.Cache;
0025: import velosurf.context.Instance;
0026: import velosurf.context.RowIterator;
0027: import velosurf.context.ExternalObjectWrapper;
0028: import velosurf.sql.Database;
0029: import velosurf.sql.PooledPreparedStatement;
0030: import velosurf.sql.SqlUtil;
0031: import velosurf.util.Logger;
0032: import velosurf.util.StringLists;
0033: import velosurf.util.UserContext;
0034: import velosurf.validation.FieldConstraint;
0035:
0036: import org.apache.commons.lang.StringEscapeUtils;
0037:
0038: /** The Entity class represents an entity in the data model.
0039: *
0040: * @author <a href=mailto:claude.brisson@gmail.com>Claude Brisson</a>
0041: *
0042: */
0043: public class Entity {
0044: /** Constructor reserved for the framework.
0045: *
0046: * @param db database connection
0047: * @param name entity name
0048: * @param readOnly access mode (read-write or read-only)
0049: * @param cachingMethod caching method to be used
0050: */
0051: public Entity(Database db, String name, boolean readOnly,
0052: int cachingMethod) {
0053: this .db = db;
0054: this .name = name;
0055: table = name; // default mapped table has same name
0056: this .readOnly = readOnly;
0057: this .cachingMethod = cachingMethod;
0058: if (this .cachingMethod != Cache.NO_CACHE)
0059: cache = new Cache(this .cachingMethod);
0060: instanceClass = Instance.class;
0061: }
0062:
0063: /** Add a column at the end of the sequential list of named columns. Called during the reverse engeenering of the database.
0064: *
0065: * @param colName column name
0066: */
0067: public void addColumn(String colName, int sqlType) {
0068: colName = db.adaptCase(colName);
0069: columns.add(colName);
0070: types.put(colName, sqlType);
0071: /* if (colnames as aliases) */aliases.put(colName, colName);
0072: }
0073:
0074: /**
0075: * Add a column alias.
0076: * @param alias alias
0077: * @param column column
0078: */
0079: public void addAlias(String alias, String column) {
0080: alias = db.adaptCase(alias);
0081: column = db.adaptCase(column);
0082: Logger.trace("added alias " + name + "." + alias + " -> "
0083: + name + "." + column);
0084: aliases.put(alias, column);
0085: }
0086:
0087: /**
0088: * Translates an alias to its column name.
0089: * @param alias alias
0090: * @return column name
0091: */
0092: public String resolveName(String alias) {
0093: alias = db.adaptCase(alias);
0094: String name = aliases.get(alias);
0095: return name == null ? alias : name;
0096: }
0097:
0098: /** Add a key column to the sequential list of the key columns. Called during the reverse-engeenering of the database.
0099: *
0100: * @param colName name of the key column
0101: */
0102: public void addPKColumn(String colName) {
0103: /* remember the alias */
0104: keyCols.add(colName);
0105: }
0106:
0107: /** Add a new attribute.
0108: * @param attribute attribute
0109: */
0110: public void addAttribute(Attribute attribute) {
0111: String name = attribute.getName();
0112: if (attributeMap.containsKey(name)) {
0113: Logger.warn("Ignoring second definition for attribute "
0114: + getName() + "." + name + "!");
0115: } else {
0116: attributeMap.put(db.adaptCase(name), attribute);
0117: Logger.trace("defined attribute " + this .name + "." + name
0118: + " = " + attribute);
0119: }
0120: }
0121:
0122: /** Get a named attribute.
0123: *
0124: * @param property attribute name
0125: * @return the attribute
0126: */
0127: public Attribute getAttribute(String property) {
0128: return (Attribute) attributeMap.get(db.adaptCase(property));
0129: }
0130:
0131: /**
0132: * Add an action.
0133: * @param action action
0134: */
0135: public void addAction(Action action) {
0136: String name = action.getName();
0137: actionMap.put(db.adaptCase(name), action);
0138: Logger.trace("defined action " + this .name + "." + name + " = "
0139: + action);
0140: }
0141:
0142: /** get an action.
0143: *
0144: * @param property action name
0145: * @return the action
0146: */
0147: public Action getAction(String property) {
0148: return (Action) actionMap.get(db.adaptCase(property));
0149: }
0150:
0151: /** Specify a custom class to use when instanciating this entity.
0152: *
0153: * @param className the java class name
0154: */
0155: public void setInstanceClass(String className) {
0156: try {
0157: instanceClass = Class.forName(className);
0158: } catch (Exception e) {
0159: Logger.log(e);
0160: }
0161: }
0162:
0163: /** Specify the caching method. See {@link Cache} for allowed constants.
0164: *
0165: * @param caching Caching method
0166: */
0167: public void setCachingMethod(int caching) {
0168: if (cachingMethod != caching) {
0169: cachingMethod = caching;
0170: if (cachingMethod == Cache.NO_CACHE)
0171: cache = null;
0172: else
0173: cache = new Cache(cachingMethod);
0174: }
0175: }
0176:
0177: /**
0178: * Add a constraint.
0179: * @param column column name
0180: * @param constraint constraint
0181: */
0182: public void addConstraint(String column, FieldConstraint constraint) {
0183: ;
0184: column = resolveName(column);
0185: Logger.trace("adding constraint on column "
0186: + Database.adaptContextCase(getName()) + "." + column
0187: + ": " + constraint);
0188: List<FieldConstraint> list = constraints.get(column);
0189: if (list == null) {
0190: list = new ArrayList<FieldConstraint>();
0191: constraints.put(column, list);
0192: }
0193: list.add(constraint);
0194: }
0195:
0196: /** Used by the framework to notify this entity that its reverse enginering is over.
0197: */
0198: public void reverseEnginered() {
0199: if (obfuscate && keyCols.size() > 0) {
0200: keyColObfuscated = new boolean[keyCols.size()];
0201: Iterator key = keyCols.iterator();
0202: int i = 0;
0203: for (; key.hasNext(); i++)
0204: keyColObfuscated[i] = obfuscatedColumns.contains(key
0205: .next());
0206: }
0207: /* fills the cache for the full caching method */
0208: if (cachingMethod == Cache.FULL_CACHE) {
0209: try {
0210: query().getRows();
0211: } catch (SQLException sqle) {
0212: Logger.error("full caching for entity " + getName()
0213: + ": could not fill the cache!");
0214: Logger.log(sqle);
0215: }
0216: }
0217: }
0218:
0219: /** Clear the cache (not used for now).
0220: */
0221: protected void clearCache() {
0222: if (cache != null)
0223: cache.clear();
0224: }
0225:
0226: /** Create a new realisation of this entity.
0227: *
0228: * @return the newly created instance
0229: */
0230: public Instance newInstance() {
0231: Instance result = null;
0232: try {
0233: if (Instance.class.isAssignableFrom(instanceClass)) {
0234: try {
0235: result = (Instance) instanceClass.newInstance();
0236: result.initialize(this );
0237: } catch (Exception e) {
0238: Constructor instanceConstructor = instanceClass
0239: .getConstructor(new Class[] { Entity.class });
0240: result = (Instance) instanceConstructor
0241: .newInstance(new Object[] { this });
0242: }
0243: } else {
0244: result = new ExternalObjectWrapper(this , instanceClass
0245: .newInstance());
0246: }
0247: } catch (Exception e) {
0248: Logger.error("could not create a new instance for entity "
0249: + getName());
0250: Logger.log(e);
0251: result = null;
0252: }
0253: return result;
0254: }
0255:
0256: /** Build a new instance from a Map object.
0257: *
0258: * @param values the Map object containing the values
0259: * @return the newly created instance
0260: */
0261: public Instance newInstance(Map<String, Object> values) {
0262: return newInstance(values, false);
0263: }
0264:
0265: /** Build a new instance from a Map object.
0266: *
0267: * @param values the Map object containing the values
0268: * @param useSQLnames map keys use SQL column names that must be translated to aliases
0269: * @return the newly created instance
0270: */
0271: public Instance newInstance(Map<String, Object> values,
0272: boolean useSQLnames) {
0273: try {
0274: Instance result = newInstance();
0275: extractColumnValues(values, result, useSQLnames);
0276: if (cachingMethod != Cache.NO_CACHE) {
0277: Object key = buildKey(result);
0278: if (key != null) {
0279: cache.put(key, result);
0280: }
0281: }
0282: return result;
0283: } catch (SQLException sqle) {
0284: Logger.log(sqle);
0285: return null;
0286: }
0287: }
0288:
0289: /**
0290: * Invalidate an instance in the cache.
0291: * @param instance instance
0292: * @throws SQLException
0293: */
0294: public void invalidateInstance(Map<String, Object> instance)
0295: throws SQLException {
0296: if (cachingMethod != Cache.NO_CACHE) {
0297: Object key = buildKey(instance);
0298: if (key != null) {
0299: cache.invalidate(key);
0300: }
0301: }
0302: }
0303:
0304: /** Extract column values from an input Map source and store result in target.
0305: *
0306: * @param source Map source object
0307: * @param target Map target object
0308: * @param SQLNames the source uses SQL names
0309: */
0310: private void extractColumnValues(Map<String, Object> source,
0311: Map<String, Object> target, boolean SQLNames)
0312: throws SQLException {
0313: /* TODO: cache a case-insensitive version of the columns list and iterate on source keys, with equalsIgnoreCase (or more efficient) funtion */
0314: /* We use keySet and not entrySet here because if the source map is a ReadOnlyMap, entrySet is not available */
0315: for (String key : source.keySet()) {
0316:
0317: /* resove anyway */
0318: String col = resolveName(key);
0319: /* this is more or less a hack: we do filter columns
0320: only when SQLNames is false. The purpose of this
0321: is to allow additionnal fields in SQL attributes
0322: returning rowsets of entities. */
0323:
0324: if (!SQLNames && !isColumn(col)) {
0325: continue;
0326: }
0327:
0328: Object val = source.get(key);
0329: if (val == null || (val.getClass().isArray())) {
0330: continue;
0331: }
0332: target.put(col, val);
0333: }
0334: }
0335:
0336: /** Build the key for the Cache from a Map.
0337: *
0338: * @param values the Map containing all values (unaliased)
0339: * @exception SQLException the getter of the Map throws an
0340: * SQLException
0341: * @return an array containing all key values
0342: */
0343: private Object buildKey(Map<String, Object> values)
0344: throws SQLException {
0345: if (keyCols.size() == 0)
0346: return null;
0347: Object[] key = new Object[keyCols.size()];
0348: int c = 0;
0349: for (String keycol : keyCols) {
0350: Object v = values.get(keycol);
0351: if (v == null) {
0352: return null;
0353: }
0354: key[c++] = values.get(keycol);
0355: }
0356: return key;
0357: }
0358:
0359: /** Getter for the name of this entity.
0360: *
0361: * @return the name of the entity
0362: */
0363: public String getName() {
0364: return name;
0365: }
0366:
0367: /** Getter for the list of key column names.
0368: *
0369: * @return the list of key column names
0370: */
0371: public List<String> getPKCols() {
0372: return keyCols;
0373: }
0374:
0375: /** Getter for the list of column names.
0376: *
0377: * @return the list of column names
0378: */
0379: public List<String> getColumns() {
0380: return columns;
0381: }
0382:
0383: public boolean isColumn(String name) {
0384: return columns.contains(name);
0385: }
0386:
0387: /** Check if the provided map contains all key columns
0388: *
0389: * @param values map of values to check
0390: * @return true if all key columns are present
0391: */
0392: /* not used
0393: public boolean hasKey(Map<String,Object> values) {
0394: if(keyCols.size() == 0) {
0395: return false; // could be 'true' but indicates that 'fetch' cannot be called
0396: }
0397: List<String> cols = new ArrayList<String>();
0398: for(String key:values.keySet()) {
0399: cols.add(resolveName(key));
0400: }
0401: return (cols.containsAll(keyCols));
0402: }
0403: */
0404: /** Insert a new row based on values of a map.
0405: *
0406: * @param values the Map object containing the values
0407: * @return success indicator
0408: */
0409: public boolean insert(Map<String, Object> values)
0410: throws SQLException {
0411: if (readOnly) {
0412: Logger.error("Error: Entity " + getName()
0413: + " is read-only!");
0414: return false;
0415: }
0416: Instance instance = newInstance(values);
0417: /* if found in cache because it exists, driver will issue a SQLException TODO review this*/
0418: boolean success = instance.insert();
0419: if (success && keyCols.size() == 1) {
0420: /* update last insert id */
0421: /* TODO review usecase, this should maybe be forbidden sometimes */
0422: /* FIXME for now, we catch some exceptions */
0423: try {
0424: String pk = keyCols.get(0);
0425: values.put(pk, instance.get(pk));
0426: // catch(UnsupportedOperationException uoe)
0427: } catch (Exception e) {
0428: Logger.warn("insert: encountered " + e.getMessage()
0429: + " while setting last inserted id value");
0430: Logger
0431: .warn("insert: values are probably provided using a read-only map");
0432: Logger
0433: .warn("you can probably just ignore this warning, this is a reminder for the dev community");
0434: }
0435: }
0436: /* try again to put it in the cache since previous attempt may have failed
0437: in case there are auto-incremented columns */
0438: if (success && cachingMethod != Cache.NO_CACHE) {
0439: Object key = buildKey(instance);
0440: if (key != null) {
0441: cache.put(key, instance);
0442: }
0443: }
0444:
0445: return success;
0446: }
0447:
0448: /** Update a row based on a set of values that must contain key values.
0449: *
0450: * @param values the Map object containing the values
0451: * @return success indicator
0452: */
0453: public boolean update(Map<String, Object> values)
0454: throws SQLException {
0455: if (readOnly) {
0456: Logger.error("Error: Entity " + getName()
0457: + " is read-only!");
0458: return false;
0459: }
0460: Instance instance = newInstance(values);
0461: return instance.update();
0462: }
0463:
0464: /** Delete a row based on (key) values.
0465: *
0466: * @param values the Map containing the values
0467: * @return success indicator
0468: */
0469: public boolean delete(Map<String, Object> values)
0470: throws SQLException {
0471: if (readOnly) {
0472: Logger.error("Error: Entity " + getName()
0473: + " is read-only!");
0474: return false;
0475: }
0476: Instance instance = newInstance(values);
0477: return instance.delete();
0478: }
0479:
0480: /** Delete a row based on the unique key string value.
0481: *
0482: * @param keyValue key value
0483: * @return success indicator
0484: */
0485: public boolean delete(String keyValue) throws SQLException {
0486: if (readOnly) {
0487: Logger.error("Error: Entity " + getName()
0488: + " is read-only!");
0489: return false;
0490: }
0491: if (keyCols.size() != 1) {
0492: if (keyCols.size() == 0)
0493: throw new SQLException("Entity.delete: Error: Entity '"
0494: + name + "' has no primary key!");
0495: else
0496: throw new SQLException("Entity.delete: Error: Entity '"
0497: + name + "' has a multi-column primary key!");
0498: }
0499: Instance instance = newInstance();
0500: instance.put(keyCols.get(0), keyValue);
0501: return instance.delete();
0502: }
0503:
0504: /** Delete a row based on the unique key string value.
0505: *
0506: * @param keyValue key value
0507: * @return success indicator
0508: */
0509: public boolean delete(Number keyValue) throws SQLException {
0510: if (readOnly) {
0511: Logger.error("Error: Entity " + getName()
0512: + " is read-only!");
0513: return false;
0514: }
0515: if (keyCols.size() != 1) {
0516: if (keyCols.size() == 0)
0517: throw new SQLException("Entity.delete: Error: Entity '"
0518: + name + "' has no primary key!");
0519: else
0520: throw new SQLException("Entity.delete: Error: Entity '"
0521: + name + "' has a multi-column primary key!");
0522: }
0523: Instance instance = newInstance();
0524: instance.put(keyCols.get(0), keyValue);
0525: return instance.delete();
0526: }
0527:
0528: /** Fetch an instance from key values stored in a List in natural order.
0529: *
0530: * @param values the List containing the key values
0531: * @return the fetched instance
0532: */
0533: public Instance fetch(List<Object> values) throws SQLException {
0534: if (values.size() != keyCols.size())
0535: throw new SQLException(
0536: "Entity.fetch: Error: Wrong number of values for '"
0537: + name + "' primary key! Got "
0538: + values.size() + ", was expecting "
0539: + keyCols.size() + " for key list: "
0540: + StringLists.join(keyCols, ","));
0541: Instance instance = null;
0542: // try in cache
0543: if (cachingMethod != Cache.NO_CACHE)
0544: instance = (Instance) cache.get(values.toArray());
0545: if (instance == null) {
0546: if (fetchQuery == null)
0547: buildFetchQuery();
0548: PooledPreparedStatement statement = db.prepare(fetchQuery);
0549: if (obfuscate) {
0550: values = new ArrayList<Object>(values);
0551: for (int col = 0; col < keyColObfuscated.length; col++)
0552: if (keyColObfuscated[col])
0553: values.set(col, deobfuscate(values.get(col)));
0554: }
0555: instance = (Instance) statement.fetch(values, this );
0556: }
0557: return instance;
0558: }
0559:
0560: /** Fetch an instance from key values stored in a Map.
0561: *
0562: * @param values the Map containing the key values
0563: * @return the fetched instance
0564: */
0565: public Instance fetch(Map<String, Object> values)
0566: throws SQLException {
0567: if (keyCols.size() == 0) {
0568: throw new SQLException(
0569: "entity "
0570: + name
0571: + ": cannot fetch an instance for an entity without key!");
0572: }
0573: Instance instance = null;
0574: /* extract key values */
0575: Object key[] = new Object[keyCols.size()];
0576: int n = 0;
0577: for (Map.Entry<String, Object> entry : values.entrySet()) {
0578: String col = resolveName(entry.getKey());
0579: int i = keyCols.indexOf(col);
0580: if (i != -1) {
0581: key[i] = entry.getValue();
0582: n++;
0583: }
0584: }
0585: if (n != keyCols.size()) {
0586: String missing = "";
0587: for (int c = 0; c < key.length; c++)
0588: if (key[c] == null)
0589: missing += keyCols.get(c) + " ";
0590: throw new SQLException("entity " + name
0591: + ".fetch(): missing key values! Missing values: "
0592: + missing);
0593: }
0594: if (cachingMethod != Cache.NO_CACHE) {
0595: // try in cache
0596: instance = (Instance) cache.get(key);
0597: }
0598: if (instance == null) {
0599: if (fetchQuery == null)
0600: buildFetchQuery();
0601: PooledPreparedStatement statement = db.prepare(fetchQuery);
0602: if (obfuscate) {
0603: for (int c = 0; c < keyCols.size(); c++) {
0604: if (isObfuscated(keyCols.get(c))) {
0605: key[c] = deobfuscate(key[c]);
0606: }
0607: }
0608: }
0609: instance = (Instance) statement.fetch(Arrays.asList(key),
0610: this );
0611: }
0612: return instance;
0613: }
0614:
0615: /** Fetch an instance from its key value as a string.
0616: *
0617: * @param keyValue the key
0618: * @return the fetched instance
0619: */
0620: public Instance fetch(String keyValue) throws SQLException {
0621: if (keyCols.size() != 1) {
0622: if (keyCols.size() == 0)
0623: throw new SQLException("Entity.fetch: Error: Entity '"
0624: + name + "' has no primary key!");
0625: else
0626: throw new SQLException("Entity.fetch: Error: Entity '"
0627: + name + "' has a multi-column primary key!");
0628: }
0629: Instance instance = null;
0630: // try in cache
0631: if (cachingMethod != Cache.NO_CACHE)
0632: // try in cache
0633: instance = (Instance) cache.get(new Object[] { keyValue });
0634: if (instance == null) {
0635: if (fetchQuery == null)
0636: buildFetchQuery();
0637: PooledPreparedStatement statement = db.prepare(fetchQuery);
0638: if (obfuscate && keyColObfuscated[0]) {
0639: keyValue = deobfuscate(keyValue);
0640: }
0641: List<String> params = new ArrayList<String>();
0642: params.add(keyValue);
0643: instance = (Instance) statement.fetch(params, this );
0644: }
0645: return instance;
0646: }
0647:
0648: /** Fetch an instance from its key value specified as a Number.
0649: *
0650: * @param keyValue the key
0651: * @return the fetched instance
0652: */
0653: public Instance fetch(Number keyValue) throws SQLException {
0654: if (keyCols.size() != 1) {
0655: if (keyCols.size() == 0)
0656: throw new SQLException("Entity.fetch: Error: Entity '"
0657: + name + "' has no primary key!");
0658: else
0659: throw new SQLException("Entity.fetch: Error: Entity '"
0660: + name + "' has a multi-column primary key!");
0661: }
0662: Instance instance = null;
0663: // try in cache
0664: if (cachingMethod != Cache.NO_CACHE)
0665: // try in cache
0666: instance = (Instance) cache.get(new Object[] { keyValue });
0667: if (instance == null) {
0668: if (fetchQuery == null)
0669: buildFetchQuery();
0670: PooledPreparedStatement statement = db.prepare(fetchQuery);
0671: List<Number> params = new ArrayList<Number>();
0672: params.add(keyValue);
0673: if (obfuscate && keyColObfuscated[0]) {
0674: Logger.warn("fetch: column '" + columns.get(0)
0675: + "' is obfuscated, please use $db." + name
0676: + ".fetch($db.obfuscate(" + keyValue + "))");
0677: return null;
0678: }
0679: instance = (Instance) statement.fetch(params, this );
0680: }
0681: return instance;
0682: }
0683:
0684: /** Get the SQL query string used to fetch one instance of this query.
0685: *
0686: * @return the SLQ query
0687: */
0688: public String getFetchQuery() {
0689: if (fetchQuery == null)
0690: buildFetchQuery();
0691: return fetchQuery;
0692: }
0693:
0694: /** Build the SQL query used to fetch one instance of this query.
0695: */
0696: private void buildFetchQuery() {
0697: List<String> whereClause = new ArrayList<String>();
0698: for (String column : keyCols) {
0699: whereClause.add(column + "=?");
0700: }
0701: fetchQuery = "select * from " + table + " where "
0702: + StringLists.join(whereClause, " and ");
0703: }
0704:
0705: /** Issue a query to iterate though all instances of this entity.
0706: *
0707: * @return the resulting RowIterator
0708: */
0709: public RowIterator query() throws SQLException {
0710: return query(null, null);
0711: }
0712:
0713: /** Issue a query to iterate thought instances of this entity, with a facultative refining criteria and a facultative order by clause.
0714: *
0715: * @param refineCriteria a refining criteria or null to get all instances
0716: * @param order an 'order by' clause or null to get instances in their
0717: * natural order
0718: * @return the resulting RowIterator
0719: */
0720: public RowIterator query(List refineCriteria, String order)
0721: throws SQLException {
0722: String query = "select * from " + table;
0723: if (refineCriteria != null)
0724: query = SqlUtil.refineQuery(query, refineCriteria);
0725: if (order != null && order.length() > 0)
0726: query = SqlUtil.orderQuery(query, order);
0727: return db.query(query, this );
0728: }
0729:
0730: /** Get the database connection.
0731: *
0732: * @return the database connection
0733: */
0734: public Database getDB() {
0735: return db;
0736: }
0737:
0738: /** Is this entity read-only or read-write?
0739: *
0740: * @return whether this entity is read-only or not
0741: */
0742: public boolean isReadOnly() {
0743: return readOnly;
0744: }
0745:
0746: /** Set this entity to be read-only or read-write.
0747: *
0748: * @param readOnly the mode to switch to : true for read-only, false for
0749: * read-write
0750: */
0751: public void setReadOnly(boolean readOnly) {
0752: this .readOnly = readOnly;
0753: }
0754:
0755: /** set the name of the table mapped by this entity.
0756: *
0757: * @param table the table mapped by this entity
0758: * read-write
0759: */
0760: public void setTableName(String table) {
0761: this .table = table;
0762: }
0763:
0764: /** Get the name of the mapped table.
0765: *
0766: * @return name of the mapped table
0767: */
0768: public String getTableName() {
0769: return table;
0770: }
0771:
0772: /** Indicates a column as being obfuscated.
0773: * @param columns list of obfuscated columns
0774: */
0775: public void setObfuscated(List<String> columns) {
0776: obfuscate = true;
0777: obfuscatedColumns = columns;
0778: }
0779:
0780: /** Returns whether the given column is obfuscated.
0781: * @param column the name of the column
0782: * @return a boolean indicating whether this column is obfuscated
0783: */
0784: public boolean isObfuscated(String column) {
0785: return obfuscate
0786: && obfuscatedColumns.contains(db.adaptCase(column));
0787: }
0788:
0789: /** Obfuscate given value.
0790: * @param value value to obfuscate
0791: *
0792: * @return obfuscated value
0793: */
0794: public String obfuscate(Object value) {
0795: return db.obfuscate(value);
0796: }
0797:
0798: /** Obfuscate this id value if needed.
0799: * @param id id value
0800: * @return filtered id value (that is, obfuscated if needed)
0801: */
0802: public Object filterID(Long id) {
0803: if (keyCols.size() == 1
0804: && isObfuscated((String) keyCols.get(0)))
0805: return obfuscate(Long.valueOf(id));
0806: return Long.valueOf(id);
0807: }
0808:
0809: /** De-obfuscate given value.
0810: * @param value value to de-obfuscate
0811: *
0812: * @return obfuscated value
0813: */
0814: public String deobfuscate(Object value) {
0815: return db.deobfuscate(value);
0816: }
0817:
0818: /** Indicates a column as being localized.
0819: * @param columns list of localized columns
0820: */
0821: public void setLocalized(List columns) {
0822: localizedColumns = columns;
0823: }
0824:
0825: /** Returns whether the given column is obfuscated.
0826: * @param column the name of the column
0827: * @return a boolean indicating whether this column is obfuscated
0828: */
0829: public boolean isLocalized(String column) {
0830: return localizedColumns != null
0831: && localizedColumns.contains(db.adaptCase(column));
0832: }
0833:
0834: /** Does this entity have localized columns?
0835: */
0836: public boolean hasLocalizedColumns() {
0837: return localizedColumns != null && localizedColumns.size() > 0;
0838: }
0839:
0840: /**
0841: * Truncate validation error messages to a maximum number of characters.
0842: */
0843: private static final int MAX_DATA_DISPLAY_LENGTH = 40;
0844:
0845: /** Validate a set of values.
0846: */
0847: public boolean validate(Map<String, Object> row)
0848: throws SQLException {
0849: boolean ret = true;
0850:
0851: UserContext userContext = db.getUserContext();
0852: /* FIXME Is it a good choice to clear the user context now?
0853: We may want to validate several entities
0854: before displaying errors to the user.
0855: */
0856: userContext.clearValidationErrors();
0857: List<ValidationError> errors = new ArrayList<ValidationError>();
0858: for (Map.Entry<String, Object> entry : row.entrySet()) {
0859: String col = resolveName(entry.getKey());
0860: Object data = entry.getValue();
0861: List<FieldConstraint> list = constraints.get(col);
0862: if (list != null) {
0863: for (FieldConstraint constraint : list) {
0864: if (!constraint.validate(data, userContext
0865: .getLocale())) {
0866: String stringData = data.toString();
0867: String formatted = (data == null
0868: || stringData.length() == 0 ? "empty value"
0869: : stringData);
0870: if (formatted.length() > MAX_DATA_DISPLAY_LENGTH) {
0871: formatted = formatted.substring(0,
0872: MAX_DATA_DISPLAY_LENGTH)
0873: + "...";
0874: }
0875: formatted = StringEscapeUtils
0876: .escapeHtml(formatted);
0877: errors.add(new ValidationError(col, userContext
0878: .localize(constraint.getMessage(),
0879: Database.adaptContextCase(col),
0880: formatted)));
0881: ret = false;
0882: }
0883: }
0884: }
0885: }
0886: if (errors.size() > 0) {
0887: /* sort in columns natural order... better than nothing.
0888: The ideal ordering would be the order of the form fields,
0889: but it is unreachable. */
0890: Collections.sort(errors);
0891: for (ValidationError error : errors) {
0892: Logger.trace("validation: new message: "
0893: + error.message);
0894: userContext.addValidationError(error.message);
0895: }
0896: }
0897: return ret;
0898: }
0899:
0900: class ValidationError implements Comparable<ValidationError> {
0901: ValidationError(String column, String message) {
0902: index = columns.indexOf(column);
0903: this .message = message;
0904: }
0905:
0906: public int compareTo(ValidationError cmp) {
0907: return index - cmp.index;
0908: }
0909:
0910: String message;
0911: int index;
0912: }
0913:
0914: /**
0915: * Check for the existence of an imported key with the same columns.
0916: * @param pkEntity primary key entity
0917: * @param fkCols foreign key columns
0918: * @return previously defined imported key, if any
0919: */
0920: public ImportedKey findImportedKey(Entity pkEntity,
0921: List<String> fkCols) {
0922: for (Map.Entry<String, Attribute> entry : attributeMap
0923: .entrySet()) {
0924: Attribute attribute = entry.getValue();
0925: if (!(attribute instanceof ImportedKey)) {
0926: continue;
0927: }
0928: ImportedKey imported = (ImportedKey) attribute;
0929: if (imported.getResultEntity().equals(pkEntity.getName())
0930: && (imported.getFKCols() == null || imported
0931: .getFKCols().equals(fkCols))) {
0932: return imported;
0933: }
0934: }
0935: return null;
0936: }
0937:
0938: /**
0939: * Check for the existence of an exported key with the same columns.
0940: * @param fkEntity foreign key entity
0941: * @param fkCols foreign key columns
0942: * @return previously defined exported key, if any
0943: */
0944: public ExportedKey findExportedKey(Entity fkEntity,
0945: List<String> fkCols) {
0946: for (Map.Entry<String, Attribute> entry : attributeMap
0947: .entrySet()) {
0948: Attribute attribute = entry.getValue();
0949: if (!(attribute instanceof ExportedKey)) {
0950: continue;
0951: }
0952: ExportedKey exported = (ExportedKey) attribute;
0953: if (exported.getResultEntity().equals(fkEntity.getName())
0954: && (exported.getFKCols() == null || exported
0955: .getFKCols().equals(fkCols))) {
0956: return exported;
0957: }
0958: }
0959: return null;
0960: }
0961:
0962: public Object filterIncomingValue(String column, Object value) {
0963: if (value == null) {
0964: return null;
0965: }
0966: /* for now, only filter boolean values */
0967: Integer type = types.get(column);
0968: if (type == null) {
0969: return value;
0970: }
0971: if (type == Types.BOOLEAN || type == Types.BIT) {
0972: if (String.class.isAssignableFrom(value.getClass())) {
0973: String s = (String) value;
0974: if ("true".equalsIgnoreCase(s)
0975: || "on".equalsIgnoreCase(s)
0976: || "1".equalsIgnoreCase(s)
0977: || "yes".equalsIgnoreCase(s)) {
0978: value = new Boolean(true);
0979: } else {
0980: value = new Boolean(false);
0981: }
0982: }
0983: }
0984: return value;
0985: }
0986:
0987: /** Name.
0988: */
0989: private String name = null;
0990: /** Table.
0991: */
0992: private String table = null;
0993: /** Column names in natural order.
0994: */
0995: private List<String> columns = new ArrayList<String>(); // list<String>
0996: /** Column types
0997: */
0998: private Map<String, Integer> types = new HashMap<String, Integer>();
0999: /** Key column names in natural order.
1000: */
1001: private List<String> keyCols = new ArrayList<String>();
1002: /** Whether to obfuscate something.
1003: */
1004: private boolean obfuscate = false;
1005: /** Names of obfuscated columns.
1006: */
1007: private List<String> obfuscatedColumns = null;
1008: /** Obfuscation status of key columns.
1009: */
1010: private boolean keyColObfuscated[] = null;
1011: /** Localized columns.
1012: */
1013: private List localizedColumns = null;
1014: /** Attributes of this entity.
1015: */
1016:
1017: /**
1018: * Column by alias map.
1019: */
1020: private Map<String, String> aliases = new HashMap<String, String>();
1021:
1022: /** Attribute map.
1023: *
1024: */
1025: private Map<String, Attribute> attributeMap = new HashMap<String, Attribute>();
1026: /** action map.
1027: */
1028: private Map<String, Action> actionMap = new HashMap<String, Action>();
1029: /** the java class to use to realize this instance.
1030: */
1031: private Class instanceClass = null;
1032: /** the SQL query used to fetch one instance of this entity.
1033: */
1034: private String fetchQuery = null;
1035: /** whether this entity is read-only or not.
1036: */
1037: private boolean readOnly;
1038: /** the database connection.
1039: */
1040: private Database db = null;
1041: /** the caching method.
1042: */
1043: private int cachingMethod = 0;
1044: /** the cache.
1045: */
1046: private Cache cache = null;
1047:
1048: /** Constraint by column name map.
1049: */
1050: private Map<String, List<FieldConstraint>> constraints = new HashMap<String, List<FieldConstraint>>();
1051: }
|