001: /*
002: * Copyright 2006-2007 The Scriptella Project Team.
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: package scriptella.jdbc;
017:
018: import scriptella.util.IOUtils;
019: import scriptella.util.StringUtils;
020:
021: import java.io.ByteArrayInputStream;
022: import java.io.ByteArrayOutputStream;
023: import java.io.Closeable;
024: import java.io.File;
025: import java.io.FileInputStream;
026: import java.io.FileNotFoundException;
027: import java.io.FileOutputStream;
028: import java.io.IOException;
029: import java.io.InputStream;
030: import java.io.InputStreamReader;
031: import java.io.OutputStream;
032: import java.io.OutputStreamWriter;
033: import java.io.Reader;
034: import java.io.StringReader;
035: import java.io.UnsupportedEncodingException;
036: import java.io.Writer;
037: import java.net.URL;
038: import java.net.URLConnection;
039: import java.sql.Blob;
040: import java.sql.Clob;
041: import java.sql.SQLException;
042:
043: /**
044: * Factory for LOBs.
045: *
046: * @author Fyodor Kupolov
047: * @version 1.0
048: */
049: class Lobs {
050: private Lobs() { //Singleton
051: }
052:
053: /**
054: * Create a new read-only BLOB for specified input stream.
055: * <p>The stream will be lazily read into memory or saved on disk depending on its length.
056: *
057: * @param is input stream to create blob.
058: * @return read-only Blob instance.
059: */
060: public static Blob newBlob(InputStream is) {
061: return new ReadonlyBlob(is);
062: }
063:
064: /**
065: * Create a new read-only BLOB for specified input stream.
066: * <p>The stream will be lazily read into memory or saved on disk depending on its length.
067: *
068: * @param is input stream to create blob.
069: * @param length input stream length.
070: * @return read-only Blob instance.
071: */
072: public static Blob newBlob(InputStream is, long length) {
073: return new ReadonlyBlob(is, length);
074: }
075:
076: /**
077: * Create a new read-only BLOB for specified URL.
078: * <p>The URL content will be lazily fetched into memory or saved on disk depending on its length.
079: *
080: * @param url URL of the blob content.
081: * @return read-only Blob instance.
082: * @see #newBlob(java.io.InputStream)
083: */
084: public static Blob newBlob(URL url) {
085: return new UrlBlob(url);
086: }
087:
088: /**
089: * Create a new read-only CLOB for specified reader.
090: * <p>The reader will be lazily read into memory or saved on disk depending on its length.
091: *
092: * @param reader reader to create CLOB.
093: * @return read-only Clob instance.
094: */
095: public static Clob newClob(Reader reader) {
096: return new ReadonlyClob(reader);
097: }
098:
099: /**
100: * Create a new read-only CLOB for specified reader.
101: * <p>The reader will be lazily read into memory or saved on disk depending on its length.
102: *
103: * @param reader reader to create CLOB.
104: * @param length content length.
105: * @return read-only Clob instance.
106: */
107: public static Clob newClob(Reader reader, long length) {
108: return new ReadonlyClob(reader, length);
109: }
110:
111: /**
112: * Base class for LOBs.
113: * <p>Defines a common functionality and helper methods.
114: */
115: static abstract class AbstractLob<T extends Closeable> implements
116: Closeable {
117: static int LOB_MAX_MEM = 100 * 1024; //100Kb
118: protected File tmpFile;
119: protected long length = -1;
120: protected T source;
121:
122: /**
123: * For custom instantiation.
124: */
125: protected AbstractLob() {
126: }
127:
128: protected AbstractLob(T source) {
129: if (source == null) {
130: throw new IllegalArgumentException(
131: "Input source cannot be null");
132: }
133: this .source = source;
134: }
135:
136: protected AbstractLob(T source, long length) {
137: if (source == null) {
138: throw new IllegalArgumentException(
139: "Input source cannot be null");
140: }
141: if (length < 0) {
142: throw new IllegalArgumentException(
143: "Input source length cannot be negative");
144: }
145: this .source = source;
146: this .length = length;
147: }
148:
149: public void close() {
150: if (tmpFile != null) {
151: tmpFile.delete();
152: tmpFile = null;
153: }
154: }
155:
156: public final void free() throws SQLException {
157: close();
158: }
159:
160: /**
161: * Read bytes/chars from source stream/reader.
162: *
163: * @param inmemory true if data should be read into a memory.
164: * @return number of bytes/chars read.
165: * @throws IOException
166: */
167: protected abstract int read(boolean inmemory)
168: throws IOException;
169:
170: /**
171: * Flush the data stored in a memory to disk.
172: * <p>This method is invoked 0 or 1 time.
173: *
174: * @throws IOException
175: */
176: protected abstract void flushToDisk() throws IOException;
177:
178: /**
179: * Invoked when initialization phase complete.
180: * <p>Subclasses should close unused and opened resources.
181: */
182: protected abstract void onInitComplete();
183:
184: /**
185: * Performs a LOB initialization in following steps:
186: * <ul>
187: * <li>Reads content into memory until the size exceeds {@link #LOB_MAX_MEM}.
188: * <li>{@link #flushToDisk() Flushes} memory content to disk.
189: * <li>Copy the left bytes to disk.
190: * </ul>
191: */
192: protected void init() {
193: if (source == null && length >= 0) {
194: return;
195: }
196:
197: int n;
198: try {
199: for (length = 0; (n = read(true)) >= 0;) {
200: length += n;
201: if (length > LOB_MAX_MEM) {
202: break;
203: }
204: }
205: if (n >= 0) {
206: flushToDisk();
207: for (; (n = read(false)) >= 0;) {
208: length += n;
209: }
210: }
211: } catch (IOException e) {
212: throw new JdbcException(
213: "Cannot initialize temprorary file storage", e);
214: } finally {
215: IOUtils.closeSilently(source);
216: source = null;
217: onInitComplete();
218: }
219: }
220:
221: /**
222: * Returns true if this LOB is stored in memory.
223: */
224: public boolean isInMemory() {
225: return tmpFile == null;
226: }
227:
228: /**
229: * Creates a temprorary file.
230: *
231: * @return output stream for temprorary file created.
232: * @throws IOException if I/O error occurs.
233: */
234: protected OutputStream createTempFile() throws IOException {
235: tmpFile = File.createTempFile("blob_", null);
236: tmpFile.deleteOnExit();
237: return new FileOutputStream(tmpFile);
238: }
239:
240: /**
241: * Returns an input stream for temprorary file.
242: */
243: protected InputStream getTempFileInputStream() {
244: if (tmpFile == null) {
245: throw new IllegalStateException(
246: "Internal error - temprorary file was not created");
247: }
248: try {
249: return new FileInputStream(tmpFile);
250: } catch (FileNotFoundException e) {
251: throw new JdbcException(
252: "Cannot open stream - temprorary file has been removed",
253: e);
254: }
255: }
256:
257: /**
258: * Returns the length of this LOB.
259: */
260: public long length() {
261: if (length < 0) {
262: init();
263: }
264: return length;
265:
266: }
267:
268: }
269:
270: /**
271: * Represents a BLOB located at specified URL.
272: */
273: static class UrlBlob extends ReadonlyBlob {
274: private URL url;
275:
276: public UrlBlob(URL url) {
277: if (url == null) {
278: throw new IllegalArgumentException("URL cannot be null");
279: }
280: this .url = url;
281: }
282:
283: URL getUrl() {
284: return url;
285: }
286:
287: @Override
288: protected void init() {
289: if (length < 0) {
290: try {
291: final URLConnection c = url.openConnection();
292: source = c.getInputStream();
293: length = c.getContentLength();
294: if (length < 0) { //if length is undefined - fetch the url
295: super .init();
296: }
297: } catch (IOException e) {
298: throw new JdbcException(
299: "Unable to read content for file " + url
300: + ": " + e.getMessage(), e);
301: }
302: }
303: }
304:
305: @Override
306: public InputStream getBinaryStream() {
307: InputStream src = source;
308: if (src != null) {
309: source = null;
310: return src;
311: } else {
312: length = -1;
313: init();
314: src = source;
315: source = null;
316: return (src == null) ? super .getBinaryStream() : src;
317: }
318: }
319: }
320:
321: /**
322: * Readonly implementation of {@link java.sql.Blob}.
323: *
324: * @author Fyodor Kupolov
325: * @version 1.0
326: */
327: static class ReadonlyBlob extends AbstractLob<InputStream>
328: implements Blob {
329: private byte[] bytes;
330: private byte[] buffer = new byte[8192];
331: private ByteArrayOutputStream memStream;
332: private OutputStream diskStream;
333: private static final byte[] EMPTY_BYTES = new byte[0];
334:
335: /**
336: * For custom instantion.
337: */
338: protected ReadonlyBlob() {
339: }
340:
341: public ReadonlyBlob(InputStream source) {
342: super (source);
343: }
344:
345: public ReadonlyBlob(InputStream source, long length) {
346: super (source, length);
347: }
348:
349: protected int read(boolean inmemory) throws IOException {
350: int n = source.read(buffer);
351: if (n > 0) {
352: if (inmemory) {
353: if (memStream == null) {
354: memStream = new ByteArrayOutputStream(n);
355: }
356: memStream.write(buffer, 0, n);
357: } else {
358: diskStream.write(buffer, 0, n);
359: }
360: }
361: return n;
362: }
363:
364: protected void flushToDisk() throws IOException {
365: diskStream = createTempFile();
366: memStream.writeTo(diskStream);
367: memStream = null;
368: }
369:
370: protected void onInitComplete() {
371: if (diskStream != null) { //If large content
372: IOUtils.closeSilently(diskStream);
373: diskStream = null;
374: } else { //otherwise in-memory content
375: //don't forget to check memStream for null in case of empty input stream
376: bytes = memStream == null ? EMPTY_BYTES : memStream
377: .toByteArray();
378: memStream = null;
379: }
380: }
381:
382: public InputStream getBinaryStream() {
383: init();
384: if (isInMemory()) {
385: return new ByteArrayInputStream(bytes);
386: } else {
387: return getTempFileInputStream();
388: }
389: }
390:
391: public void close() {
392: super .close();
393: if (bytes != null) {
394: bytes = null;
395: }
396: }
397:
398: public String toString() {
399: try {
400: return "BLOB: "
401: + StringUtils.consoleFormat(new String(IOUtils
402: .toByteArray(getBinaryStream(), 1024)));
403: } catch (Exception e) {
404: return "BLOB: " + e;
405: }
406: }
407:
408: //--------------- Unsupported methods
409: public byte[] getBytes(long pos, int length)
410: throws SQLException {
411: throw new SQLException("Unsupported operation"); //Due to performance reasons
412: }
413:
414: public long position(byte pattern[], long start)
415: throws SQLException {
416: throw new SQLException("Unsupported operation");
417: }
418:
419: public long position(Blob pattern, long start)
420: throws SQLException {
421: throw new SQLException("Unsupported operation");
422: }
423:
424: public int setBytes(long pos, byte[] bytes) throws SQLException {
425: throw new SQLException("Unsupported operation");
426: }
427:
428: public int setBytes(long pos, byte[] bytes, int offset, int len)
429: throws SQLException {
430: throw new SQLException("Unsupported operation");
431: }
432:
433: public OutputStream setBinaryStream(long pos)
434: throws SQLException {
435: throw new SQLException("Unsupported operation");
436: }
437:
438: public void truncate(long len) throws SQLException {
439: throw new SQLException("Unsupported operation");
440: }
441:
442: public InputStream getBinaryStream(long pos, long length)
443: throws SQLException {
444: throw new SQLException("Unsupported operation");
445: }
446:
447: }
448:
449: /**
450: * Represents a read-only CLOB.
451: */
452: static class ReadonlyClob extends AbstractLob<Reader> implements
453: Clob {
454: private String string;
455: private char[] buffer = new char[8192];
456: private StringBuilder mem;
457: private Writer diskWriter;
458:
459: public ReadonlyClob(Reader source) {
460: super (source);
461: }
462:
463: public ReadonlyClob(Reader source, long length) {
464: super (source, length);
465: }
466:
467: protected int read(boolean inmemory) throws IOException {
468: int n = source.read(buffer);
469: if (n > 0) {
470: if (inmemory) {
471: if (mem == null) {
472: mem = new StringBuilder(n);
473: }
474: mem.append(buffer, 0, n);
475: } else {
476: diskWriter.write(buffer, 0, n);
477: }
478: }
479: return n;
480: }
481:
482: protected void flushToDisk() throws IOException {
483: diskWriter = new OutputStreamWriter(createTempFile(),
484: "UTF-8");
485: diskWriter.append(mem);
486: mem = null;
487: }
488:
489: protected void onInitComplete() {
490: if (diskWriter != null) {
491: IOUtils.closeSilently(diskWriter);
492: diskWriter = null;
493: } else { //otherwise in-memory content
494: //don't forget to check mem buffer for null in case of empty input reader
495: string = mem == null ? "" : mem.toString();
496: mem = null;
497: }
498: }
499:
500: public Reader getCharacterStream() {
501: init();
502: if (isInMemory()) {
503: return new StringReader(string);
504: } else {
505: try {
506: return new InputStreamReader(
507: getTempFileInputStream(), "UTF-8");
508: } catch (UnsupportedEncodingException e) {
509: throw new IllegalStateException(e); //should never happen
510: }
511: }
512: }
513:
514: /**
515: * For debug purposes.
516: */
517: public String toString() {
518: try {
519: return "CLOB: "
520: + StringUtils.consoleFormat(IOUtils.toString(
521: getCharacterStream(), 1024));
522: } catch (Exception e) {
523: return "CLOB: " + e;
524: }
525: }
526:
527: //--------------- Unsupported methods
528: public String getSubString(long pos, int length)
529: throws SQLException {
530: throw new SQLException("Unsupported operation");
531: }
532:
533: public InputStream getAsciiStream() throws SQLException {
534: throw new SQLException("Unsupported operation");
535: }
536:
537: public long position(String searchstr, long start)
538: throws SQLException {
539: throw new SQLException("Unsupported operation");
540: }
541:
542: public long position(Clob searchstr, long start)
543: throws SQLException {
544: throw new SQLException("Unsupported operation");
545: }
546:
547: public int setString(long pos, String str) throws SQLException {
548: throw new SQLException("Unsupported operation");
549: }
550:
551: public int setString(long pos, String str, int offset, int len)
552: throws SQLException {
553: throw new SQLException("Unsupported operation");
554: }
555:
556: public OutputStream setAsciiStream(long pos)
557: throws SQLException {
558: throw new SQLException("Unsupported operation");
559: }
560:
561: public Writer setCharacterStream(long pos) throws SQLException {
562: throw new SQLException("Unsupported operation");
563: }
564:
565: public void truncate(long len) throws SQLException {
566: throw new SQLException("Unsupported operation");
567: }
568:
569: public Reader getCharacterStream(long pos, long length)
570: throws SQLException {
571: throw new SQLException("Unsupported operation");
572: }
573: }
574:
575: }
|