001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package com.sun.rave.faces.data;
043:
044: import java.io.Serializable;
045: import java.util.AbstractMap;
046: import java.util.AbstractSet;
047: import java.util.ArrayList;
048: import java.util.Collection;
049: import java.util.HashMap;
050: import java.util.Iterator;
051: import java.util.List;
052: import java.util.Map;
053: import java.util.Set;
054:
055: /**
056: * <p>Cache for row and column values that supports persisting the data behind
057: * a <code>UIData</code> component across HTTP requests, without
058: * requiring that the underlying JDBC connection remain open. It also includes
059: * mechanisms to detect which row and column values have been updated, in order
060: * to support minimal database activity when synchronizing these changes to
061: * the underlying database.</p>
062: *
063: * <p><code>DataCache</code> declares itself to be <code>Serializable</code>
064: * to conform to the J2EE platform requirement that session scope attributes
065: * should be serializable on a distributable container. However, this will
066: * only succeed if the actual cached column values are themselves
067: * Serializable as well.</p>
068: *
069: * @author craigmcc
070: */
071: public class DataCache implements Serializable {
072:
073: // ------------------------------------------------------ Instance Variables
074:
075: /**
076: * <p>The cached <code>Row</code> information, keyed by row index
077: * (wrapped in a <code>java.lang.Integer</code>.</p>
078: */
079: private Map cache = new HashMap();
080:
081: // ---------------------------------------------------------- Public Methods
082:
083: /**
084: * <p>Add a new row entry to the cache, replacing any existing cache entry
085: * for the same <code>rowIndex</code> value.</p>
086: *
087: * @param rowIndex Row index this row corresponds to
088: * @param row <code>DataCache.Row</code> to be added
089: */
090: public void add(int rowIndex, Row row) {
091:
092: cache.put(new Integer(rowIndex), row);
093:
094: }
095:
096: /**
097: * <p>Clear any cached row and column data.</p>
098: */
099: public void clear() {
100:
101: cache.clear();
102:
103: }
104:
105: /**
106: * <p>Commit the updated state of all cached rows. After this
107: * method completes, the current values for all columns will
108: * appear to be original, any rows marked deleted will be
109: * removed, and no row will be considered updated.</p>
110: */
111: public void commit() {
112:
113: List deletedKeys = new ArrayList();
114:
115: // Commit any updated rows
116: Iterator keys = cache.keySet().iterator();
117: while (keys.hasNext()) {
118: Integer key = (Integer) keys.next();
119: Row row = (Row) cache.get(key);
120: if (row.isDeleted()) {
121: deletedKeys.add(key);
122: } else if (row.isUpdated()) {
123: row.commit();
124: }
125: }
126:
127: // Remove any deleted rows
128: keys = deletedKeys.iterator();
129: while (keys.hasNext()) {
130: Integer key = (Integer) keys.next();
131: cache.remove(key);
132: }
133:
134: }
135:
136: /**
137: * <p>Create and return a <code>Column</code> instance configured
138: * with the specified parameters.</p>
139: *
140: * @param schemaName Schema name of the table containing this
141: * column (if any)
142: * @param tableName Table name containing this column (if any)
143: * @param columnName Column name of this column
144: * @param sqlType SQL type (from java.sql.Types)
145: * @param original Original value for this column
146: */
147: public Column createColumn(String schemaName, String tableName,
148: String columnName, int sqlType, Object original) {
149:
150: return new Column(schemaName, tableName, columnName, sqlType,
151: original);
152:
153: }
154:
155: /**
156: * <p>Create and return a <code>Column</code> instance configured
157: * with the specified parameters.</p>
158: *
159: * @param schemaName Schema name of the table containing this
160: * column (if any)
161: * @param tableName Table name containing this column (if any)
162: * @param columnName Column name of this column
163: * @param sqlType SQL type (from java.sql.Types)
164: * @param javaType Java type as would be returned by
165: * Class.forName(ResultSetMetaData.getColumnClassName)
166: * @param original Original value for this column
167: */
168: public Column createColumn(String schemaName, String tableName,
169: String columnName, int sqlType, Class javaType,
170: Object original) {
171:
172: return new Column(schemaName, tableName, columnName, sqlType,
173: javaType, original);
174:
175: }
176:
177: /**
178: * <p>Create and return a <code>Row</code> instance configured
179: * with the specified parameters.</p>
180: *
181: * @param columns <code>Column</code> instances for this row
182: */
183: public Row createRow(Column columns[]) {
184:
185: return new Row(columns);
186:
187: }
188:
189: /**
190: * <p>Return the cached row associated with the specified row index,
191: * if any; otherwise, return <code>null</code>.</p>
192: *
193: * @param rowIndex Row index for which to retrieve a row
194: */
195: public Row get(int rowIndex) {
196:
197: return (Row) cache.get(new Integer(rowIndex));
198:
199: }
200:
201: /**
202: * <p>Return an <code>Iterator</code> over the row index values
203: * (of type <code>java.lang.Integer</code>) for which cached data
204: * is present.</p>
205: S */
206: public Iterator iterator() {
207:
208: return cache.keySet().iterator();
209:
210: }
211:
212: /**
213: * <p>Remove any row entry corresponding to the specified
214: * <code>rowIndex</code>.</p>
215: *
216: * @param rowIndex Row index for which to remove any cached data
217: */
218: public void remove(int rowIndex) {
219:
220: cache.remove(new Integer(rowIndex));
221:
222: }
223:
224: /**
225: * <p>Reset the updated state of all rows and columns in the cache.</p>
226: */
227: public void reset() {
228:
229: Iterator rows = cache.values().iterator();
230: while (rows.hasNext()) {
231: Row row = (Row) rows.next();
232: row.reset();
233: }
234:
235: }
236:
237: // --------------------------------------------------------- Private Methods
238:
239: // --------------------------------------------------- Public Helper Classes
240:
241: /**
242: * <p><code>Column</code> encapsulates the stored information
243: * about a single "column" of data, typically corresponding to a column
244: * from an individual row in an underlying relational database.</p>
245: */
246: public class Column implements Map.Entry, Serializable {
247:
248: /**
249: * <p>Construct a new <code>DataCache.Column</code> configured
250: * by the specified parameters.</p>
251: *
252: * @param schemaName Schema name for the table containing
253: * this column (if any)
254: * @param tableName Table name containing this column
255: * (if any)
256: * @param key Column name for this column
257: * @param sqlType SQL type (from java.sql.Types)
258: * @param original Original data value for this column
259: */
260: Column(String schemaName, String tableName, String key,
261: int sqlType, Object original) {
262: this (schemaName, tableName, key, sqlType, null, original);
263: }
264:
265: /**
266: * <p>Construct a new <code>DataCache.Column</code> configured
267: * by the specified parameters.</p>
268: *
269: * @param schemaName Schema name for the table containing
270: * this column (if any)
271: * @param tableName Table name containing this column
272: * (if any)
273: * @param key Column name for this column
274: * @param sqlType SQL type (from java.sql.Types)
275: * @param javaType Java type as would be returned from
276: * Class.forName(ResultSetMetaData.getColumnClassName)
277: * @param original Original data value for this column
278: */
279: Column(String schemaName, String tableName, String key,
280: int sqlType, Class javaType, Object original) {
281: this .schemaName = schemaName;
282: this .tableName = tableName;
283: this .key = key;
284: this .sqlType = sqlType;
285: this .javaType = javaType;
286: this .original = original;
287: this .replacement = null;
288: this .updated = false;
289: }
290:
291: private String key;
292: private Object original;
293: private int sqlType;
294: private Class javaType;
295: private Object replacement;
296: private String schemaName;
297: private String tableName;
298: private boolean updated;
299:
300: /**
301: * <p>If this column value has been updated, copy the
302: * current value to the original value and clear the
303: * updated state. Otherwise, do nothing.</p>
304: */
305: public void commit() {
306: if (updated) {
307: original = replacement;
308: replacement = null;
309: updated = false;
310: }
311: }
312:
313: /**
314: * <p>Return the column name for this column as a String.
315: * This is a type-safe alias for <code>getKey()</code>.</p>
316: */
317: public String getColumnName() {
318: return (String) key;
319: }
320:
321: /**
322: * <p>Return the column name for this column.</p>
323: */
324: public Object getKey() {
325: return key;
326: }
327:
328: /**
329: * <p>Return the SQL type for this column.
330: */
331: public int getSqlType() {
332: return sqlType;
333: }
334:
335: /**
336: * <p>Return the Java type for this column.
337: */
338: public Class getJavaType() {
339: return javaType;
340: }
341:
342: /**
343: * <p>Return the original value for this column.</p>
344: */
345: public Object getOriginal() {
346: return original;
347: }
348:
349: /**
350: * <p>Return the replacement value for this column, if any.
351: * This is only meaningful if <code>isUpdated()</code>
352: * returns <code>true</code>.</p>
353: */
354: public Object getReplacement() {
355: return replacement;
356: }
357:
358: /**
359: * <p>Return the name of the schema containing the table
360: * containing this column (if any); otherwise, return
361: * <code>null</code>.</p>
362: */
363: public String getSchemaName() {
364: return schemaName;
365: }
366:
367: /**
368: * <p>Return the name of the table containing this column
369: * (if any); otherwise, return <code>null</code>.</p>
370: */
371: public String getTableName() {
372: return tableName;
373: }
374:
375: /**
376: * <p>Return the replacement value if this column has been
377: * updated; else return the original value.</p>
378: */
379: public Object getValue() {
380: if (updated) {
381: return replacement;
382: } else {
383: return original;
384: }
385: }
386:
387: /**
388: * <p>Return <code>true</code> if the value for this column
389: * has been updated.</p>
390: */
391: public boolean isUpdated() {
392: return updated;
393: }
394:
395: /**
396: * <p>Reset the updated state of this column, and throw away any
397: * reference to a replacement value.</p>
398: */
399: public void reset() {
400: replacement = null;
401: updated = false;
402: }
403:
404: /**
405: * <p>If the specified value is different from the original
406: * value, save it and mark this column (and the containing
407: * row) as having been updated. Otherwise, take no action.</p>
408: *
409: * @param obj Replacement value for this column
410: */
411: public Object setValue(Object obj) {
412: if (original == null) {
413: if (obj != null) {
414: /*
415: * !JK If we are updating a null to an empty String, don't do that.
416: * As it stands now, null values are getting overwritten with
417: * empty Strings. This isn't typically the behavior desired. Long
418: * term, we need submit to be showing null values for null objects
419: * that haven't been updated.
420: */
421: if (!(obj instanceof String && ((String) obj)
422: .length() == 0)) {
423: update(obj);
424: }
425: }
426: } else {
427: if (obj == null) {
428: update(obj);
429: } else if (!original.equals(obj)) {
430: update(obj);
431: }
432: }
433: return original;
434: }
435:
436: /**
437: * <p>Save the specified replacement value, and mark this
438: * column (and the associated row) as having been modified.</p>
439: *
440: * @param obj Replacement value for this column
441: */
442: private void update(Object obj) {
443: replacement = obj;
444: updated = true;
445: }
446:
447: }
448:
449: /**
450: * <p><code>Row</code> encapsulates the stored information
451: * about a single "row" of data, typically corresponding to a row in
452: * an underlying relational database. The implementation methods that
453: * perform comparisons against column name values are done so in
454: * a case-insensitive manner. No modification to the set of
455: * <code>Column</code>s included in a <code>Row</code> is permitted
456: * after construction.</p>
457: */
458: public class Row extends AbstractMap implements Serializable {
459:
460: /**
461: * <p>Construct a new <code>Row</code> wrapping the specified
462: * <code>Column</code> values.</p>
463: *
464: * @param columns <code>Column</code> entries for this row
465: */
466: Row(Column columns[]) {
467: this .columns = columns;
468: this .deleted = false;
469: }
470:
471: private Column columns[];
472: private boolean deleted;
473:
474: // ---------- Row Methods ----------
475:
476: /**
477: * <p>Call <code>commit()</code> on all of the included
478: * columns in order to make the current values be the
479: * original ones, and reset the updated state. Set the
480: * <code>deleted</code> state of this row to <code>false</code>.</p>
481: */
482: public void commit() {
483: for (int i = 0; i < columns.length; i++) {
484: columns[i].commit();
485: }
486: setDeleted(false);
487: }
488:
489: /**
490: * <p>Return the <code>Column</code> objects representing
491: * the column values in this row.</p>
492: */
493: Column[] getColumns() {
494: return this .columns;
495: }
496:
497: /**
498: * <P>Return <code>true</code> if this row has been
499: * marked for deletion.</p>
500: */
501: public boolean isDeleted() {
502: return this .deleted;
503: }
504:
505: /**
506: * <p>Return <code>true</code> if any column value in this
507: * <code>Row</code> has been updated.</p>
508: */
509: public boolean isUpdated() {
510: for (int i = 0; i < columns.length; i++) {
511: if (columns[i].updated) {
512: return true;
513: }
514: }
515: return false;
516: }
517:
518: /**
519: * <p>Reset the updated state of this row and all underlying
520: * columns. Set the <code>deleted</code> state of this row
521: * to <code>false</code>.</p>
522: */
523: public void reset() {
524: for (int i = 0; i < columns.length; i++) {
525: columns[i].reset();
526: }
527: setDeleted(false);
528: }
529:
530: /**
531: * <p>Set the <code>deleted</code> flag on this row
532: * to the specified value.</p>
533: *
534: * @param deleted The new deleted flag
535: */
536: public void setDeleted(boolean deleted) {
537: this .deleted = deleted;
538: }
539:
540: // ---------- Map Methods ----------
541:
542: public void clear() {
543: throw new UnsupportedOperationException();
544: }
545:
546: // Case-insensitive match on column name
547: public boolean containsKey(Object key) {
548: String skey = (String) key;
549: for (int i = 0; i < columns.length; i++) {
550: if (skey.equalsIgnoreCase(columns[i].getColumnName())) {
551: return true;
552: }
553: }
554: return false;
555: }
556:
557: public Set entrySet() {
558: return new ColumnSet(this );
559: }
560:
561: // Case-insensitive match on column name
562: public Object get(Object key) {
563: String skey = (String) key;
564: for (int i = 0; i < columns.length; i++) {
565: if (skey.equalsIgnoreCase(columns[i].getColumnName())) {
566: return columns[i].getValue();
567: }
568: }
569: return null;
570: }
571:
572: // Case-insensitive match on column name
573: public Object put(Object key, Object value) {
574: String skey = (String) key;
575: for (int i = 0; i < columns.length; i++) {
576: if (skey.equalsIgnoreCase(columns[i].getColumnName())) {
577: Object previous = columns[i].getValue();
578: columns[i].setValue(value);
579: return previous;
580: }
581: }
582: throw new IllegalArgumentException(skey);
583: }
584:
585: public Object remove(Object key) {
586: throw new UnsupportedOperationException();
587: }
588:
589: }
590:
591: // -------------------------------------------------- Private Helper Classes
592:
593: /**
594: * <p><code>Iterator</code> over the <code>Column</code>
595: * entries for the specified <code>Row</code>.</p>
596: */
597: private class ColumnIterator implements Iterator {
598:
599: ColumnIterator(Row row) {
600: this .row = row;
601: }
602:
603: int index = 0;
604: private Row row;
605:
606: public boolean hasNext() {
607: return index < row.columns.length;
608: }
609:
610: public Object next() {
611: return row.columns[index++];
612: }
613:
614: public void remove() {
615: throw new UnsupportedOperationException();
616: }
617:
618: }
619:
620: /**
621: * <p><code>Set</code> representing the <code>Column</code>
622: * entries for the specified <code>Row</code>.</p>
623: */
624: private class ColumnSet extends AbstractSet {
625:
626: ColumnSet(Row row) {
627: this .row = row;
628: }
629:
630: private Row row;
631:
632: // ---------- Set Methods ----------
633:
634: public boolean add(Object o) {
635: throw new UnsupportedOperationException();
636: }
637:
638: public boolean addAll(Collection c) {
639: throw new UnsupportedOperationException();
640: }
641:
642: public void clear() {
643: throw new UnsupportedOperationException();
644: }
645:
646: public Iterator iterator() {
647: return new ColumnIterator(row);
648: }
649:
650: public boolean remove(Object o) {
651: throw new UnsupportedOperationException();
652: }
653:
654: public boolean removeAll(Collection c) {
655: throw new UnsupportedOperationException();
656: }
657:
658: public boolean retainAll(Collection c) {
659: throw new UnsupportedOperationException();
660: }
661:
662: public int size() {
663: return row.columns.length;
664: }
665:
666: }
667:
668: }
|