001: /* Copyright (c) 2001-2005, The HSQL Development Group
002: * All rights reserved.
003: *
004: * Redistribution and use in source and binary forms, with or without
005: * modification, are permitted provided that the following conditions are met:
006: *
007: * Redistributions of source code must retain the above copyright notice, this
008: * list of conditions and the following disclaimer.
009: *
010: * Redistributions in binary form must reproduce the above copyright notice,
011: * this list of conditions and the following disclaimer in the documentation
012: * and/or other materials provided with the distribution.
013: *
014: * Neither the name of the HSQL Development Group nor the names of its
015: * contributors may be used to endorse or promote products derived from this
016: * software without specific prior written permission.
017: *
018: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
019: * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
020: * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
021: * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
022: * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
023: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
024: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025: * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
026: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027: * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029: */
030:
031: package org.hsqldb.persist;
032:
033: import java.io.IOException;
034: import java.io.UnsupportedEncodingException;
035:
036: import org.hsqldb.HsqlException;
037: import org.hsqldb.Table;
038: import org.hsqldb.Trace;
039: import org.hsqldb.lib.FileUtil;
040: import org.hsqldb.lib.HsqlByteArrayOutputStream;
041: import org.hsqldb.rowio.RowInputInterface;
042: import org.hsqldb.rowio.RowInputText;
043: import org.hsqldb.rowio.RowInputTextQuoted;
044: import org.hsqldb.rowio.RowOutputText;
045: import org.hsqldb.rowio.RowOutputTextQuoted;
046: import org.hsqldb.scriptio.ScriptWriterText;
047: import org.hsqldb.store.ObjectCacheHashMap;
048: import org.hsqldb.Database;
049:
050: // Ito Kazumitsu 20030328 - patch 1.7.2 - character encoding support
051: // Dimitri Maziuk - patch for NL in string support
052: // sqlbob@users - updated for 1.8.0 to allow new-lines in fields
053: // fredt@users - updated for 1.8.0 to allow correct behaviour with transactions
054:
055: /**
056: * Acts as a buffer manager for a single TEXT table with respect its Row data.<p>
057: *
058: * Handles read/write operations on the table's text format data file using a
059: * compatible pair of org.hsqldb.rowio input/output class instances.
060: *
061: *
062: * @author sqlbob@users (RMP)
063: * @version 1.8.0
064: * @since 1.7.0
065: */
066: public class TextCache extends DataFileCache {
067:
068: //state of Cache
069: public static final String NL = System
070: .getProperty("line.separator");
071: public String fs;
072: public String vs;
073: public String lvs;
074: public String stringEncoding;
075: public boolean isQuoted;
076: public boolean isAllQuoted;
077: public boolean ignoreFirst;
078: protected String header;
079: protected Table table;
080: private ObjectCacheHashMap uncommittedCache;
081:
082: //
083: final static char DOUBLE_QUOTE_CHAR = '\"';
084: final static char BACKSLASH_CHAR = '\\';
085: final static char LF_CHAR = '\n';
086: final static char CR_CHAR = '\r';
087:
088: /**
089: * The source string for a cached table is evaluated and the parameters
090: * are used to open the source file.<p>
091: *
092: * Settings are used in this order: (1) settings specified in the
093: * source string for the table (2) global database settings in
094: * *.properties file (3) program defaults
095: *
096: * fredt - this used to write rows as soon as they are inserted
097: * but now this is subject to session autoCommit / or commit
098: * storeOnInsert = true;
099: */
100: TextCache(Table table, String name) throws HsqlException {
101:
102: super (table.database, name);
103:
104: this .table = table;
105: uncommittedCache = new ObjectCacheHashMap(5);
106: }
107:
108: protected void initParams(Database database, String baseFileName)
109: throws HsqlException {
110:
111: fileName = baseFileName;
112: this .database = database;
113: fa = FileUtil.getDefaultInstance();
114:
115: HsqlProperties tableprops = HsqlProperties
116: .delimitedArgPairsToProps(fileName, "=", ";", null);
117:
118: //-- Get file name
119: switch (tableprops.errorCodes.length) {
120:
121: case 0:
122: throw Trace.error(Trace.TEXT_TABLE_SOURCE,
123: Trace.TEXT_TABLE_SOURCE_FILENAME);
124: case 1:
125:
126: // source file name is the only key without a value
127: fileName = tableprops.errorKeys[0].trim();
128: break;
129:
130: default:
131: throw Trace.error(Trace.TEXT_TABLE_SOURCE,
132: Trace.TEXT_TABLE_SOURCE_VALUE_MISSING,
133: tableprops.errorKeys[1]);
134: }
135:
136: //-- Get separators:
137: HsqlDatabaseProperties dbProps = database.getProperties();
138:
139: fs = translateSep(tableprops.getProperty("fs", dbProps
140: .getProperty(HsqlDatabaseProperties.textdb_fs, ",")));
141: vs = translateSep(tableprops.getProperty("vs", dbProps
142: .getProperty(HsqlDatabaseProperties.textdb_vs, fs)));
143: lvs = translateSep(tableprops.getProperty("lvs", dbProps
144: .getProperty(HsqlDatabaseProperties.textdb_lvs, fs)));
145:
146: if (fs.length() == 0 || vs.length() == 0 || lvs.length() == 0) {
147: throw Trace.error(Trace.TEXT_TABLE_SOURCE,
148: Trace.TEXT_TABLE_SOURCE_SEPARATOR);
149: }
150:
151: //-- Get booleans
152: ignoreFirst = tableprops.isPropertyTrue("ignore_first", dbProps
153: .isPropertyTrue(
154: HsqlDatabaseProperties.textdb_ignore_first,
155: false));
156: isQuoted = tableprops.isPropertyTrue("quoted", dbProps
157: .isPropertyTrue(HsqlDatabaseProperties.textdb_quoted,
158: true));
159: isAllQuoted = tableprops
160: .isPropertyTrue(
161: "all_quoted",
162: dbProps
163: .isPropertyTrue(
164: HsqlDatabaseProperties.textdb_all_quoted,
165: false));
166:
167: //-- Get encoding
168: stringEncoding = translateSep(tableprops.getProperty(
169: "encoding", dbProps
170: .getProperty(
171: HsqlDatabaseProperties.textdb_encoding,
172: "ASCII")));
173:
174: //-- Get size and scale
175: int cacheScale = tableprops.getIntegerProperty("cache_scale",
176: dbProps.getIntegerProperty(
177: HsqlDatabaseProperties.textdb_cache_scale, 10,
178: 8, 16));
179: int cacheSizeScale = tableprops.getIntegerProperty(
180: "cache_size_scale", dbProps.getIntegerProperty(
181: HsqlDatabaseProperties.textdb_cache_size_scale,
182: 10, 8, 20));
183: int lookupTableLength = 1 << cacheScale;
184: int avgRowBytes = 1 << cacheSizeScale;
185:
186: maxCacheSize = lookupTableLength * 3;
187: maxCacheBytes = maxCacheSize * avgRowBytes;
188: maxDataFileSize = Integer.MAX_VALUE;
189: cachedRowPadding = 1;
190: cacheFileScale = 1;
191: }
192:
193: protected void initBuffers() {
194:
195: if (isQuoted || isAllQuoted) {
196: rowIn = new RowInputTextQuoted(fs, vs, lvs, isAllQuoted);
197: rowOut = new RowOutputTextQuoted(fs, vs, lvs, isAllQuoted,
198: stringEncoding);
199: } else {
200: rowIn = new RowInputText(fs, vs, lvs, false);
201: rowOut = new RowOutputText(fs, vs, lvs, false,
202: stringEncoding);
203: }
204: }
205:
206: private String translateSep(String sep) {
207: return translateSep(sep, false);
208: }
209:
210: /**
211: * Translates the escaped characters in a separator string and returns
212: * the non-escaped string.
213: */
214: private String translateSep(String sep, boolean isProperty) {
215:
216: if (sep == null) {
217: return (null);
218: }
219:
220: int next = 0;
221:
222: if ((next = sep.indexOf(BACKSLASH_CHAR)) != -1) {
223: int start = 0;
224: char[] sepArray = sep.toCharArray();
225: char ch = 0;
226: int len = sep.length();
227: StringBuffer realSep = new StringBuffer(len);
228:
229: do {
230: realSep.append(sepArray, start, next - start);
231:
232: start = ++next;
233:
234: if (next >= len) {
235: realSep.append(BACKSLASH_CHAR);
236:
237: break;
238: }
239:
240: if (!isProperty) {
241: ch = sepArray[next];
242: }
243:
244: if (ch == 'n') {
245: realSep.append(LF_CHAR);
246:
247: start++;
248: } else if (ch == 'r') {
249: realSep.append(CR_CHAR);
250:
251: start++;
252: } else if (ch == 't') {
253: realSep.append('\t');
254:
255: start++;
256: } else if (ch == BACKSLASH_CHAR) {
257: realSep.append(BACKSLASH_CHAR);
258:
259: start++;
260: } else if (ch == 'u') {
261: start++;
262:
263: realSep.append((char) Integer.parseInt(sep
264: .substring(start, start + 4), 16));
265:
266: start += 4;
267: } else if (sep.startsWith("semi", next)) {
268: realSep.append(';');
269:
270: start += 4;
271: } else if (sep.startsWith("space", next)) {
272: realSep.append(' ');
273:
274: start += 5;
275: } else if (sep.startsWith("quote", next)) {
276: realSep.append(DOUBLE_QUOTE_CHAR);
277:
278: start += 5;
279: } else if (sep.startsWith("apos", next)) {
280: realSep.append('\'');
281:
282: start += 4;
283: } else {
284: realSep.append(BACKSLASH_CHAR);
285: realSep.append(sepArray[next]);
286:
287: start++;
288: }
289: } while ((next = sep.indexOf(BACKSLASH_CHAR, start)) != -1);
290:
291: realSep.append(sepArray, start, len - start);
292:
293: sep = realSep.toString();
294: }
295:
296: return sep;
297: }
298:
299: /**
300: * Opens a data source file.
301: */
302: public void open(boolean readonly) throws HsqlException {
303:
304: fileFreePosition = 0;
305:
306: try {
307: dataFile = ScaledRAFile.newScaledRAFile(database, fileName,
308: readonly, ScaledRAFile.DATA_FILE_RAF, null, null);
309: fileFreePosition = dataFile.length();
310:
311: if (fileFreePosition > Integer.MAX_VALUE) {
312: throw new IOException();
313: }
314:
315: initBuffers();
316: } catch (Exception e) {
317: throw Trace.error(Trace.FILE_IO_ERROR,
318: Trace.TextCache_openning_file_error, new Object[] {
319: fileName, e });
320: }
321:
322: cacheReadonly = readonly;
323: }
324:
325: void reopen() throws HsqlException {
326: open(cacheReadonly);
327: }
328:
329: /**
330: * Writes newly created rows to disk. In the current implentation,
331: * such rows have already been saved, so this method just removes a
332: * source file that has no rows.
333: */
334: public void close(boolean write) throws HsqlException {
335:
336: if (dataFile == null) {
337: return;
338: }
339:
340: try {
341: cache.saveAll();
342:
343: boolean empty = (dataFile.length() <= NL.length());
344:
345: dataFile.close();
346:
347: dataFile = null;
348:
349: if (empty && !cacheReadonly) {
350: FileUtil.delete(fileName);
351: }
352: } catch (Exception e) {
353: throw Trace.error(Trace.FILE_IO_ERROR,
354: Trace.TextCache_closing_file_error, new Object[] {
355: fileName, e });
356: }
357: }
358:
359: /**
360: * Closes the source file and deletes it if it is not read-only.
361: */
362: void purge() throws HsqlException {
363:
364: uncommittedCache.clear();
365:
366: try {
367: if (cacheReadonly) {
368: close(false);
369: } else {
370: if (dataFile != null) {
371: dataFile.close();
372:
373: dataFile = null;
374: }
375:
376: FileUtil.delete(fileName);
377: }
378: } catch (Exception e) {
379: throw Trace.error(Trace.FILE_IO_ERROR,
380: Trace.TextCache_purging_file_error, new Object[] {
381: fileName, e });
382: }
383: }
384:
385: /**
386: *
387: */
388: public synchronized void remove(int pos, PersistentStore store)
389: throws IOException {
390:
391: CachedObject row = (CachedObject) uncommittedCache.remove(pos);
392:
393: if (row != null) {
394: return;
395: }
396:
397: row = cache.release(pos);
398:
399: clearRowImage(row);
400: release(pos);
401: }
402:
403: private void clearRowImage(CachedObject row) throws IOException {
404:
405: int length = row.getStorageSize()
406: - ScriptWriterText.BYTES_LINE_SEP.length;
407:
408: rowOut.reset();
409:
410: HsqlByteArrayOutputStream out = rowOut.getOutputStream();
411:
412: out.fill(' ', length);
413: out.write(ScriptWriterText.BYTES_LINE_SEP);
414: dataFile.seek(row.getPos());
415: dataFile.write(out.getBuffer(), 0, out.size());
416: }
417:
418: public synchronized void removePersistence(int pos,
419: PersistentStore store) throws IOException {
420:
421: CachedObject row = (CachedObject) uncommittedCache.get(pos);
422:
423: if (row != null) {
424: return;
425: }
426:
427: row = cache.get(pos);
428:
429: clearRowImage(row);
430: }
431:
432: protected synchronized RowInputInterface readObject(int pos)
433: throws IOException {
434:
435: ByteArray buffer = new ByteArray(80);
436: boolean complete = false;
437: boolean wasCR = false;
438: int c;
439: boolean hasQuote = false;
440: boolean wasNormal = false;
441:
442: pos = findNextUsedLinePos(pos);
443:
444: if (pos == -1) {
445: return null;
446: }
447:
448: dataFile.seek(pos);
449:
450: while (!complete) {
451: wasNormal = false;
452: c = dataFile.read();
453:
454: if (c == -1) {
455: if (buffer.length() == 0) {
456: return null;
457: }
458:
459: complete = true;
460:
461: if (wasCR) {
462: break;
463: }
464:
465: if (!cacheReadonly) {
466: dataFile.write(ScriptWriterText.BYTES_LINE_SEP, 0,
467: ScriptWriterText.BYTES_LINE_SEP.length);
468: }
469:
470: break;
471: }
472:
473: switch (c) {
474:
475: case DOUBLE_QUOTE_CHAR:
476: wasNormal = true;
477: complete = wasCR;
478: wasCR = false;
479:
480: if (isQuoted) {
481: hasQuote = !hasQuote;
482: }
483: break;
484:
485: case CR_CHAR:
486: wasCR = !hasQuote;
487: break;
488:
489: case LF_CHAR:
490: complete = !hasQuote;
491: break;
492:
493: default:
494: wasNormal = true;
495: complete = wasCR;
496: wasCR = false;
497: }
498:
499: buffer.append(c);
500: }
501:
502: if (complete) {
503: int length = (int) dataFile.getFilePointer() - pos;
504:
505: if (wasNormal) {
506: length--;
507: }
508:
509: ((RowInputText) rowIn).setSource(buffer.toString(), pos,
510: length);
511:
512: return rowIn;
513: }
514:
515: return null;
516: }
517:
518: public int readHeaderLine() throws HsqlException {
519:
520: boolean complete = false;
521: boolean wasCR = false;
522: boolean wasNormal = false;
523: ByteArray buffer = new ByteArray(80);
524:
525: while (!complete) {
526: wasNormal = false;
527:
528: int c;
529:
530: try {
531: c = dataFile.read();
532:
533: if (c == -1) {
534: if (buffer.length() == 0) {
535: return 0;
536: }
537:
538: complete = true;
539:
540: if (!cacheReadonly) {
541: dataFile.write(ScriptWriterText.BYTES_LINE_SEP,
542: 0,
543: ScriptWriterText.BYTES_LINE_SEP.length);
544: }
545:
546: break;
547: }
548: } catch (IOException e) {
549: throw Trace.error(Trace.TEXT_FILE);
550: }
551:
552: switch (c) {
553:
554: case CR_CHAR:
555: wasCR = true;
556: break;
557:
558: case LF_CHAR:
559: complete = true;
560: break;
561:
562: default:
563: wasNormal = true;
564: complete = wasCR;
565: wasCR = false;
566: }
567:
568: buffer.append(c);
569: }
570:
571: header = buffer.toString();
572:
573: try {
574: int length = (int) dataFile.getFilePointer();
575:
576: if (wasNormal) {
577: length--;
578: }
579:
580: return length;
581: } catch (IOException e) {
582: throw Trace.error(Trace.TEXT_FILE);
583: }
584: }
585:
586: // fredt - new method
587:
588: /**
589: * Searches from file pointer, pos, and finds the beginning of the first
590: * line that contains any non-space character. Increments the row counter
591: * when a blank line is skipped.
592: *
593: * If none found return -1
594: */
595: int findNextUsedLinePos(int pos) throws IOException {
596:
597: int firstPos = pos;
598: int currentPos = pos;
599: boolean wasCR = false;
600:
601: dataFile.seek(pos);
602:
603: while (true) {
604: int c = dataFile.read();
605:
606: currentPos++;
607:
608: switch (c) {
609:
610: case CR_CHAR:
611: wasCR = true;
612: break;
613:
614: case LF_CHAR:
615: wasCR = false;
616:
617: ((RowInputText) rowIn).skippedLine();
618:
619: firstPos = currentPos;
620: break;
621:
622: case ' ':
623: if (wasCR) {
624: wasCR = false;
625:
626: ((RowInputText) rowIn).skippedLine();
627: }
628: break;
629:
630: case -1:
631: return -1;
632:
633: default:
634: return firstPos;
635: }
636: }
637: }
638:
639: public synchronized void add(CachedObject object)
640: throws IOException {
641: super .add(object);
642: clearRowImage(object);
643: }
644:
645: public synchronized CachedObject get(int i, PersistentStore store,
646: boolean keep) throws HsqlException {
647:
648: if (i < 0) {
649: return null;
650: }
651:
652: CachedObject o = (CachedObject) uncommittedCache.get(i);
653:
654: if (o == null) {
655: o = super .get(i, store, keep);
656: }
657:
658: /*
659: if (o == null) {
660: o = super.get(i, store, keep);
661: }
662: */
663: return o;
664: }
665:
666: /**
667: * This is called internally when old rows need to be removed from the
668: * cache. Text table rows that have not been saved are those that have not
669: * been committed yet. So we don't save them but add them to the
670: * uncommitted cache until such time that they are committed or rolled
671: * back- fredt
672: */
673: protected synchronized void saveRows(CachedObject[] rows,
674: int offset, int count) throws IOException {
675:
676: if (count == 0) {
677: return;
678: }
679:
680: for (int i = offset; i < offset + count; i++) {
681: CachedObject r = rows[i];
682:
683: uncommittedCache.put(r.getPos(), r);
684:
685: rows[i] = null;
686: }
687: }
688:
689: /**
690: * In case the row has been moved to the uncommittedCache, removes it.
691: * Then saves the row as normal.
692: */
693: public synchronized void saveRow(CachedObject row)
694: throws IOException {
695: uncommittedCache.remove(row.getPos());
696: super .saveRow(row);
697: }
698:
699: public String getHeader() {
700: return header;
701: }
702:
703: public void setHeader(String header) throws HsqlException {
704:
705: if (ignoreFirst && fileFreePosition == 0) {
706: try {
707: writeHeader(header);
708:
709: this .header = header;
710: } catch (IOException e) {
711: throw new HsqlException(e, Trace
712: .getMessage(Trace.GENERAL_IO_ERROR),
713: Trace.GENERAL_IO_ERROR);
714: }
715:
716: return;
717: }
718:
719: throw Trace.error(Trace.TEXT_TABLE_HEADER);
720: }
721:
722: private void writeHeader(String header) throws IOException {
723:
724: byte[] buf = null;
725: String firstLine = header + NL;
726:
727: try {
728: buf = firstLine.getBytes(stringEncoding);
729: } catch (UnsupportedEncodingException e) {
730: buf = firstLine.getBytes();
731: }
732:
733: dataFile.write(buf, 0, buf.length);
734:
735: fileFreePosition = buf.length;
736: }
737:
738: private class ByteArray {
739:
740: private byte[] buffer;
741: private int buflen;
742:
743: public ByteArray(int n) {
744: buffer = new byte[n];
745: buflen = 0;
746: }
747:
748: public void append(int c) {
749:
750: if (buflen >= buffer.length) {
751: byte[] newbuf = new byte[buflen + 80];
752:
753: System.arraycopy(buffer, 0, newbuf, 0, buflen);
754:
755: buffer = newbuf;
756: }
757:
758: buffer[buflen] = (byte) c;
759:
760: buflen++;
761: }
762:
763: public int length() {
764: return buflen;
765: }
766:
767: public void setLength(int l) {
768: buflen = l;
769: }
770:
771: public String toString() {
772:
773: try {
774: return new String(buffer, 0, buflen, stringEncoding);
775: } catch (UnsupportedEncodingException e) {
776: return new String(buffer, 0, buflen);
777: }
778: }
779: }
780:
781: public int getLineNumber() {
782: return ((RowInputText) rowIn).getLineNumber();
783: }
784:
785: protected void setFileModified() throws IOException {
786: fileModified = true;
787: }
788: }
|