001: /*
002: * Copyright 2003 The Apache Software Foundation.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package velosurf.context;
018:
019: import java.sql.SQLException;
020: import java.util.*;
021:
022: import velosurf.model.Action;
023: import velosurf.model.Attribute;
024: import velosurf.model.Entity;
025: import velosurf.sql.Database;
026: import velosurf.sql.PooledPreparedStatement;
027: import velosurf.util.Logger;
028: import velosurf.util.StringLists;
029: import velosurf.util.UserContext;
030:
031: /** An Instance provides field values by their name.
032: *
033: * @author <a href=mailto:claude.brisson@gmail.com>Claude Brisson</a>
034: */
035: public class Instance extends TreeMap<String, Object> {
036:
037: /** Build an empty instance for the given entity.
038: * The method initialize(Entity) should be called afterwards.
039: * @param entity Entity this instance is a realisation of
040: */
041: public Instance() {
042: }
043:
044: /** Build an empty instance for the given entity.
045: *
046: * @param entity Entity this instance is a realisation of
047: * @deprecated
048: */
049: public Instance(Entity entity) {
050: initialize(entity);
051: }
052:
053: /**
054: * Builds a generic instance using <code>values</code>.
055: * @param values
056: */
057: public Instance(Map<String, Object> values) {
058: for (Object key : values.keySet()) {
059: put(Database.adaptContextCase((String) key), values
060: .get(key));
061: }
062: }
063:
064: /** Initialization. Meant to be overloaded if needed.
065: * @param entity
066: */
067: public void initialize(Entity entity) {
068: this .entity = entity;
069: db = this .entity.getDB();
070: localized = this .entity.hasLocalizedColumns();
071: }
072:
073: /** Get this Instance's Entity.
074: *
075: * @return this Instance's Entity.
076: */
077: public EntityReference getEntity() {
078: return new EntityReference(entity);
079: }
080:
081: /** <p>Returns an ArrayList of two-entries maps ('name' & 'value'), meant to be use in a #foreach loop to build form fields.</p>
082: * <p>Example:</p>
083: * <code>
084: * #foreach ($field in $product.primaryKey)<br>
085: * <input type=hidden name='$field.name' value='$field.value'><br>
086: * #end</code>
087: * <p>Please note that this method won't be of any help if you are using column aliases.</p>
088: *
089: * @return an ArrayList of two-entries maps ('name' & 'value')
090: */
091: public List getPrimaryKey() {
092: List<Map<String, Object>> result = new ArrayList<Map<String, Object>>();
093: if (entity != null) {
094: for (Iterator i = entity.getPKCols().iterator(); i
095: .hasNext();) {
096: String key = (String) i.next();
097: Map<String, Object> map = new HashMap<String, Object>();
098: map.put("name", key);
099: map.put("value", getInternal(key));
100: result.add(map);
101: }
102: }
103: return result;
104: }
105:
106: /** <p>Generic getter, used to access this instance properties by their name.</p>
107: * <p>Asked property is first searched in the Map, then among Attributes defined for the entity.</p>
108: *
109: * @param key key of the property to be returned
110: * @return a String, an Instance, an AttributeReference or null if an error
111: * occurs
112: */
113: public Object get(Object k) {
114: String key = resolveName((String) k);
115: Object result = null;
116: try {
117: result = super .get(key);
118: if (result == null) {
119: if (entity != null) {
120: Attribute attribute = entity.getAttribute(key);
121: if (attribute != null) {
122: switch (attribute.getType()) {
123: case Attribute.ROWSET:
124: result = new AttributeReference(this ,
125: attribute);
126: // then cache it in the map, so that order and refinement will work later in the same context
127: super .put(key, result);
128: break;
129: case Attribute.ROW:
130: result = attribute.fetch(this );
131: if (attribute.getCaching()) {
132: super .put(key, result);
133: }
134: break;
135: case Attribute.SCALAR:
136: result = attribute.evaluate(this );
137: if (attribute.getCaching()) {
138: super .put(key, result);
139: }
140: break;
141: default:
142: Logger.error("Unknown attribute type for "
143: + entity.getName() + "." + key
144: + "!");
145: }
146: } else {
147: Action action = entity.getAction(key);
148: if (action != null)
149: result = Integer.valueOf(action
150: .perform(this ));
151: }
152: }
153: } else if (localized && entity.isLocalized(key)) {
154: result = db.getUserContext()
155: .localize(result.toString());
156: }
157: } catch (SQLException sqle) {
158: handleSQLException(sqle);
159: }
160: return result;
161: }
162:
163: /** Generic setter.
164: *
165: * @param key key of the property to be set
166: * @param value corresponding value
167: * @return previous value, or null
168: */
169: public synchronized Object put(String key, Object value) {
170: key = resolveName(key);
171: if (entity != null && entity.isColumn(key)) {
172: value = entity.filterIncomingValue(key, value);
173: }
174: return super .put(key, value);
175: }
176:
177: /** Global setter that will only set values the correspond to actual
178: * columns (otherwise, use putAll(Map values)).
179: *
180: * @param values corresponding values
181: */
182:
183: public synchronized void setColumns(Map<String, Object> values) {
184: if (entity == null) {
185: Logger
186: .warn("instance.putColumn(map) cannot be used when entity is null");
187: return;
188: }
189: for (Map.Entry<String, Object> entry : values.entrySet()) {
190: if (entity.isColumn(entity.resolveName(entry.getKey()))) {
191: put(entry.getKey(), entry.getValue());
192: }
193: }
194: }
195:
196: /** Internal getter. First tries on the external object then on the Map interface.
197: *
198: * @param key key of the property to be returned
199: * @return a String, an Instance, an AttributeReference or null if not found or if an error
200: * occurs
201: */
202: public Object getInternal(Object key) {
203: Object ret = getExternal(key);
204: if (ret == null)
205: ret = super .get(key);
206: return ret;
207: }
208:
209: /** External getter. Meant to be overloaded in ExternalObjectWrapper.
210: *
211: * @param key key of the property to be returned
212: * @return a String, an Instance, an AttributeReference or null if not found or if an error
213: * occurs
214: */
215: public Object getExternal(Object key) {
216: return null;
217: }
218:
219: /**
220: * Test equality of two instances.
221: * @param o other instance
222: * @return equality status
223: */
224: public boolean equals(Object o) {
225: return super .equals(o);
226: }
227:
228: /** <p>Update the row associated with this Instance from passed values.</p>
229: * <p>Velosurf will ensure all key columns are specified, to avoid an accidental massive update.</p>
230: *
231: * @return <code>true</code> if successfull, <code>false</code> if an error
232: * occurs (in which case $db.error can be checked).
233: */
234: public synchronized boolean update() {
235: try {
236: if (entity == null) {
237: throw new SQLException(
238: "Cannot update an instance whose Entity is null.");
239: }
240: if (entity.isReadOnly()) {
241: throw new SQLException("Entity " + entity.getName()
242: + " is read-only.");
243: }
244:
245: List<String> updateClause = new ArrayList<String>();
246: List<String> whereClause = new ArrayList<String>();
247: List<Object> params = new ArrayList<Object>();
248: List<String> cols = new ArrayList<String>(entity
249: .getColumns());
250: cols.removeAll(entity.getPKCols());
251: for (String col : cols) {
252: Object value = getInternal(col);
253: if (value != null) {
254: updateClause.add(col + "=?");
255: if (entity.isObfuscated(col))
256: value = entity.deobfuscate(value);
257: params.add(value);
258: }
259: }
260: if (updateClause.size() == 0) {
261: Logger
262: .warn("update of instance '"
263: + entity.getName()
264: + "' all non-key columns are null - no update will be performed");
265: // return true anyway ?
266: return true;
267: }
268: for (String col : entity.getPKCols()) {
269: Object value = getInternal(col);
270: if (value == null)
271: throw new SQLException(
272: "field '"
273: + col
274: + "' belongs to primary key and cannot be null!");
275: if (entity.isObfuscated(col))
276: value = entity.deobfuscate(value);
277: // if (entity.isLocalized(col)) value = entity.unlocalize(value); ???
278: whereClause.add(col + "=?");
279: params.add(value);
280: }
281: String query = "update " + entity.getTableName() + " set "
282: + StringLists.join(updateClause, ",") + " where "
283: + StringLists.join(whereClause, " and ");
284: PooledPreparedStatement statement = db.prepare(query);
285: int nb = statement.update(params);
286: if (nb == 0) {
287: Logger
288: .warn("query \"" + query
289: + "\" affected 0 row...");
290: } else if (nb > 1) { // ?!?! Referential integrities on key columns should avoid this...
291: throw new SQLException("query \"" + query
292: + "\" affected more than 1 rows!");
293: } else {
294: /* invalidate cache */
295: if (entity != null) {
296: entity.invalidateInstance(this );
297: }
298: }
299: return true;
300: } catch (SQLException sqle) {
301: handleSQLException(sqle);
302: return false;
303: }
304: }
305:
306: /** <p>Update the row associated with this Instance from actual values.</p>
307: * <p>Velosurf will ensure all key columns are specified, to avoid an accidental massive update.</p>
308: *
309: * @param values values to be used for the update
310: * @return <code>true</code> if successfull, <code>false</code> if an error
311: * occurs (in which case $db.error can be checked).
312: */
313: public synchronized boolean update(Map<String, Object> values) {
314: if (values != null && values != this ) {
315: setColumns(values);
316: }
317: return update();
318: }
319:
320: /** <p>Delete the row associated with this Instance.</p>
321: * <p>Velosurf will ensure all key columns are specified, to avoid an accidental massive update.</p>
322: *
323: * @return <code>true</code> if successfull, <code>false</code> if an error
324: * occurs (in which case $db.error can be checked).
325: */
326: public synchronized boolean delete() {
327: try {
328: if (entity == null)
329: throw new SQLException(
330: "Instance.delete: Error: Entity is null!");
331: List<String> whereClause = new ArrayList<String>();
332: List<Object> params = new ArrayList<Object>();
333: for (String col : entity.getPKCols()) {
334: Object value = getInternal(col);
335: if (value == null)
336: throw new SQLException(
337: "Instance.delete: Error: field '"
338: + col
339: + "' belongs to primary key and cannot be null!");
340: if (entity.isObfuscated(col))
341: value = entity.deobfuscate(value);
342: whereClause.add(col + "=?");
343: params.add(value);
344: }
345: String query = "delete from " + entity.getTableName()
346: + " where "
347: + StringLists.join(whereClause, " and ");
348: PooledPreparedStatement statement = db.prepare(query);
349: int nb = statement.update(params);
350: if (nb == 0) {
351: Logger
352: .warn("query \"" + query
353: + "\" affected 0 row...");
354: } else if (nb > 1) // ?!?! Referential integrities on key columns should avoid this...
355: throw new SQLException("query \"" + query
356: + "\" affected more than 1 rows!");
357: else {
358: /* invalidate cache */
359: if (entity != null) {
360: entity.invalidateInstance(this );
361: }
362: }
363: return true;
364: } catch (SQLException sqle) {
365: handleSQLException(sqle);
366: return false;
367: }
368: }
369:
370: /** Insert a new row corresponding to this Instance.
371: *
372: * @return <code>true</code> if successfull, <code>false</code> if an error
373: * occurs (in which case $db.error can be checked).
374: */
375: public synchronized boolean insert() {
376: try {
377: if (entity == null) {
378: throw new SQLException(
379: "Instance.insert: Error: Entity is null!");
380: }
381:
382: if (!entity.validate(this )) {
383: return false;
384: }
385: List<String> colsClause = new ArrayList<String>();
386: List<String> valsClause = new ArrayList<String>();
387: List<Object> params = new ArrayList<Object>();
388: List<String> cols = entity.getColumns();
389: for (String col : cols) {
390: Object value = getInternal(col);
391: if (value != null) {
392: colsClause.add(col);
393: valsClause.add("?");
394: if (entity.isObfuscated(col))
395: value = entity.deobfuscate(value);
396: params.add(value);
397: }
398: }
399: String query = "insert into " + entity.getTableName()
400: + " (" + StringLists.join(colsClause, ",")
401: + ") values (" + StringLists.join(valsClause, ",")
402: + ")";
403: PooledPreparedStatement statement = db.prepare(query);
404: statement.update(params);
405: List<String> keys = entity.getPKCols();
406: if (keys.size() == 1) {
407: /* What if the ID is not autoincremented? TODO check it. => reverse engineering of autoincrement, and set the value in the instance itself */
408: String keycol = keys.get(0);
409: long newid = statement.getLastInsertID();
410: db.getUserContext().setLastInsertedID(entity, newid);
411: if (getInternal(keycol) == null) {
412: put(keycol, entity.isObfuscated(keycol) ? entity
413: .obfuscate(newid) : newid);
414: }
415: }
416: return true;
417: } catch (SQLException sqle) {
418: handleSQLException(sqle);
419: return false;
420: }
421: }
422:
423: /** Validate this instance against declared contraints.
424: * @return a boolean stating whether this instance data are valid in regard to declared constraints
425: */
426: public boolean validate() {
427: try {
428: return entity.validate(this );
429: } catch (SQLException sqle) {
430: handleSQLException(sqle);
431: return false;
432: }
433: }
434:
435: /** Handle an sql exception.
436: *
437: */
438: private void handleSQLException(SQLException sqle) {
439: Logger.log(sqle);
440: db.setError(sqle.getMessage());
441: }
442:
443: protected String resolveName(String name) {
444: if (entity != null) {
445: return entity.resolveName(name);
446: } else if (db != null) {
447: return db.adaptCase(name);
448: } else {
449: return name;
450: }
451: }
452:
453: public boolean containsKey(Object key) {
454: return super .containsKey(resolveName((String) key));
455: }
456:
457: public Object remove(Object key) {
458: return super .remove(resolveName((String) key));
459: }
460:
461: /** This Instance's Entity.
462: */
463: protected Entity entity = null;
464:
465: /** Is there a column to localize?
466: */
467: private boolean localized = false;
468:
469: /** The main database connection.
470: */
471: protected Database db = null;
472:
473: }
|