1   /*
2    * Copyright 2003 - 2013 The eFaps Team
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   *
16   * Revision:        $Rev$
17   * Last Changed:    $Date$
18   * Last Changed By: $Author$
19   */
20  
21  package org.efaps.db.store;
22  
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.OutputStream;
26  import java.sql.Connection;
27  import java.sql.PreparedStatement;
28  import java.sql.ResultSet;
29  import java.sql.SQLException;
30  import java.sql.Statement;
31  import java.util.Map;
32  import java.util.zip.GZIPInputStream;
33  import java.util.zip.ZipInputStream;
34  
35  import org.efaps.db.Context;
36  import org.efaps.db.GeneralInstance;
37  import org.efaps.db.Instance;
38  import org.efaps.db.InstanceQuery;
39  import org.efaps.db.databases.AbstractDatabase;
40  import org.efaps.db.transaction.AbstractResource;
41  import org.efaps.db.transaction.ConnectionResource;
42  import org.efaps.db.wrapper.SQLPart;
43  import org.efaps.db.wrapper.SQLSelect;
44  import org.efaps.util.EFapsException;
45  import org.slf4j.Logger;
46  import org.slf4j.LoggerFactory;
47  
48  /**
49   *
50   * @author The eFaps Team
51   * @version $Id$
52   */
53  public abstract class AbstractStoreResource
54      extends AbstractResource
55      implements Resource
56  {
57      /**
58       * Name of the main store table.
59       */
60      public static final String TABLENAME_STORE = "T_CMGENSTORE";
61  
62      /**
63       * Name of the column for the filename.
64       */
65      public static final String COLNAME_FILENAME = "FILENAME";
66  
67      /**
68       * Name of the column for the file length.
69       */
70      public static final String COLNAME_FILELENGTH = "FILELENGTH";
71  
72      /**
73       * Logging instance used in this class.
74       */
75      private static final Logger LOG = LoggerFactory.getLogger(AbstractStoreResource.class);
76  
77      /**
78       * Basic SQL Select for getting the Resource.
79       */
80      private static final SQLSelect SQL_SELECT = new SQLSelect()
81                                                      .column(0, "ID")
82                                                      .column(1, AbstractStoreResource.COLNAME_FILENAME)
83                                                      .column(1, AbstractStoreResource.COLNAME_FILELENGTH)
84                                                      .column(1, "ID")
85                                                      .from(GeneralInstance.TABLENAME, 0)
86                                                      .leftJoin(AbstractStoreResource.TABLENAME_STORE, 1, "ID", 0, "ID");
87  
88      /**
89       * @see #StoreEvent
90       */
91      private StoreEvent storeEvent = StoreEvent.UNKNOWN;
92  
93      /**
94       * Buffer used to copy from the input stream to the output stream.
95       *
96       * @see #read()
97       */
98      private final byte[] buffer = new byte[1024];
99  
100     /**
101      * Instance this resource belongs to.
102      */
103     private Instance instance;
104 
105     /**
106      * GeneralID of this Store Resource.
107      */
108     private Long generalID;
109 
110     /**
111      * Do the related objects exist.
112      */
113     private boolean[] exist;
114 
115     /**
116      * File Name of the Source.
117      */
118     private String fileName = "DEFAULT";
119 
120     /**
121      * Length of the file in byte.
122      */
123     private Long fileLength = new Long(0);
124 
125     /**
126      * Store this Resource belongs to.
127      */
128     private Store store;
129 
130     /**
131      * {@inheritDoc}
132      */
133     @Override
134     public void initialize(final Instance _instance,
135                            final Store _store)
136         throws EFapsException
137     {
138         this.instance = _instance;
139         this.store = _store;
140         final SQLSelect select = AbstractStoreResource.SQL_SELECT.getCopy()
141                         .addPart(SQLPart.WHERE)
142                         .addColumnPart(0, "INSTTYPEID").addPart(SQLPart.EQUAL)
143                         .addValuePart(_instance.getType().getId())
144                         .addPart(SQLPart.AND)
145                         .addColumnPart(0, "INSTID").addPart(SQLPart.EQUAL).addValuePart(_instance.getId());
146         this.exist = new boolean[1 + add2Select(select)];
147         getGeneralID(select.getSQL());
148 
149     }
150 
151     /**
152      * {@inheritDoc}
153      */
154     @Override
155     public void open(final StoreEvent _event)
156         throws EFapsException
157     {
158         this.storeEvent = _event;
159         super.open();
160         if (getStoreEvent().equals(StoreEvent.READ) || getStoreEvent().equals(StoreEvent.WRITE)) {
161             insertDefaults();
162         }
163     }
164 
165     /**
166      * The output stream is written with the content of the file. From method {@link #read()} the input stream is used
167      * and copied into the output stream.
168      *
169      * @param _out output stream where the file content must be written
170      * @throws EFapsException if an error occurs
171      * @see #read()
172      */
173     public void read(final OutputStream _out)
174         throws EFapsException
175     {
176         StoreResourceInputStream in = null;
177         try {
178             in = (StoreResourceInputStream) read();
179             if (in != null) {
180                 int length = 1;
181                 while (length > 0) {
182                     length = in.read(this.buffer);
183                     if (length > 0) {
184                         _out.write(this.buffer, 0, length);
185                     }
186                 }
187             }
188         } catch (final IOException e) {
189             throw new EFapsException(AbstractStoreResource.class, "read.IOException", e);
190         } finally {
191             if (in != null) {
192                 try {
193                     in.closeWithoutCommit();
194                 } catch (final IOException e) {
195                     AbstractStoreResource.LOG.warn("Catched IOException in class: " + this.getClass());
196                 }
197             }
198         }
199     }
200 
201     /**
202      * {@inheritDoc}
203      */
204     @Override
205     public String getFileName()
206         throws EFapsException
207     {
208         return this.fileName;
209     }
210 
211     /**
212      * {@inheritDoc}
213      */
214     @Override
215     public Long getFileLength()
216         throws EFapsException
217     {
218         return this.fileLength;
219     }
220 
221     /**
222      * Insert default values in the table. (if necessary).
223      * @throws EFapsException on error
224      */
225     protected void insertDefaults()
226         throws EFapsException
227     {
228         if (!getExist()[0] && getGeneralID() != null) {
229             try {
230                 final ConnectionResource res = Context.getThreadContext().getConnectionResource();
231                 final Connection con = res.getConnection();
232                 Context.getDbType().newInsert(AbstractStoreResource.TABLENAME_STORE, "ID", false)
233                                 .column("ID", getGeneralID())
234                                 .column(AbstractStoreResource.COLNAME_FILENAME, "TMP")
235                                 .column(AbstractStoreResource.COLNAME_FILELENGTH, 0)
236                                 .execute(con);
237                 res.commit();
238                 this.fileName = "TMP";
239                 this.fileLength = new Long(0);
240             } catch (final SQLException e) {
241                 throw new EFapsException(AbstractStoreResource.class, "insertDefaults", e);
242             }
243         }
244     }
245 
246 
247     /**
248      * Set the info for the file in this store reosurce.
249      * @param _filename     name of the file
250      * @param _fileLength   length of the file
251      * @throws EFapsException on error
252      */
253     protected void setFileInfo(final String _filename,
254                                final long _fileLength)
255         throws EFapsException
256     {
257         if (!_filename.equals(this.fileName) || _fileLength != this.fileLength) {
258             ConnectionResource res = null;
259             try {
260                 res = Context.getThreadContext().getConnectionResource();
261                 final AbstractDatabase<?> db = Context.getDbType();
262                 final StringBuilder cmd = new StringBuilder().append(db.getSQLPart(SQLPart.UPDATE)).append(" ")
263                         .append(db.getTableQuote()).append(AbstractStoreResource.TABLENAME_STORE)
264                         .append(db.getTableQuote())
265                         .append(" ").append(db.getSQLPart(SQLPart.SET)).append(" ")
266                         .append(db.getColumnQuote())
267                         .append(AbstractStoreResource.COLNAME_FILENAME)
268                         .append(db.getColumnQuote()).append(db.getSQLPart(SQLPart.EQUAL)).append("? ")
269                         .append(db.getSQLPart(SQLPart.COMMA))
270                         .append(db.getColumnQuote())
271                         .append(AbstractStoreResource.COLNAME_FILELENGTH)
272                         .append(db.getColumnQuote()).append(db.getSQLPart(SQLPart.EQUAL)).append("? ")
273                         .append(db.getSQLPart(SQLPart.WHERE)).append(" ")
274                         .append(db.getColumnQuote()).append("ID").append(db.getColumnQuote())
275                         .append(db.getSQLPart(SQLPart.EQUAL)).append(getGeneralID());
276 
277                 final PreparedStatement stmt = res.getConnection().prepareStatement(cmd.toString());
278                 try {
279                     stmt.setString(1, _filename);
280                     stmt.setLong(2, _fileLength);
281                     stmt.execute();
282                 } finally {
283                     stmt.close();
284                 }
285                 res.commit();
286             } catch (final EFapsException e) {
287                 res.abort();
288                 throw e;
289             } catch (final SQLException e) {
290                 res.abort();
291                 throw new EFapsException(JDBCStoreResource.class, "write.SQLException", e);
292             }
293         }
294     }
295 
296     /**
297      * Add to the select for the existence check.
298      * @param _select select to add to
299      * @return number of added columns
300      */
301     protected abstract int add2Select(final SQLSelect _select);
302 
303     /**
304      * Get the generalID etc. from the eFasp DataBase.
305      * @param _complStmt Statement to be executed
306      * @throws EFapsException on error
307      */
308     private void getGeneralID(final String _complStmt)
309         throws EFapsException
310     {
311         ConnectionResource con = null;
312         try {
313             con = Context.getThreadContext().getConnectionResource();
314 
315             final Statement stmt = con.getConnection().createStatement();
316 
317             final ResultSet rs = stmt.executeQuery(_complStmt.toString());
318 
319             while (rs.next()) {
320                 this.generalID = rs.getLong(1);
321                 this.fileName = rs.getString(2);
322                 if (this.fileName != null && !this.fileName.isEmpty()) {
323                     this.fileName = this.fileName.trim();
324                 }
325                 this.fileLength = rs.getLong(3);
326                 for (int i = 0; i < this.exist.length; i++) {
327                     this.exist[i] = rs.getLong(4 + i) > 0;
328                 }
329                 getAdditionalInfo(rs);
330             }
331             rs.close();
332             stmt.close();
333             con.commit();
334         } catch (final SQLException e) {
335             throw new EFapsException(InstanceQuery.class, "executeOneCompleteStmt", e);
336         } finally {
337             if (con != null && con.isOpened()) {
338                 con.abort();
339             }
340         }
341     }
342 
343     /**
344      * Can be used by implementation to get additionla information form the database.
345      * @param _rs   ResultSet
346      * @throws SQLException on error
347      */
348     protected void getAdditionalInfo(final ResultSet _rs)
349         throws SQLException
350     {
351     }
352 
353     /**
354      * Frees this resource. Only a dummy implementation because nothing must be freed for this store.
355      */
356     @Override
357     protected void freeResource()
358     {
359     }
360 
361     /**
362      * Getter method for instance variable {@link #instance}.
363      *
364      * @return value of instance variable {@link #instance}
365      */
366     protected Instance getInstance()
367     {
368         return this.instance;
369     }
370 
371     /**
372      * Setter method for instance variable {@link #instance}.
373      *
374      * @param _instance value for instance variable {@link #instance}
375      */
376     protected void setInstance(final Instance _instance)
377     {
378         this.instance = _instance;
379     }
380 
381     /**
382      * Is this Store resource compressed.
383      *
384      * @return Is this Store resource compressed
385      */
386     protected Compress getCompress()
387     {
388         Compress compress;
389         if (this.store.getResourceProperties().containsKey(Store.PROPERTY_COMPRESS)) {
390             compress = Compress.valueOf(this.store.getResourceProperties().get(Store.PROPERTY_COMPRESS).toUpperCase());
391         } else {
392             compress = Compress.NONE;
393         }
394         return compress;
395     }
396 
397     /**
398      * Getter method for instance variable {@link #store}.
399      *
400      * @return value of instance variable {@link #store}
401      */
402     protected Store getStore()
403     {
404         return this.store;
405     }
406 
407     /**
408      * Get the properties for this resource.
409      *
410      * @return properties for this resource
411      */
412     protected Map<String, String> getProperties()
413     {
414         return this.store.getResourceProperties();
415     }
416 
417     /**
418      * Getter method for instance variable {@link #exist}.
419      *
420      * @return value of instance variable {@link #exist}
421      */
422     protected boolean[] getExist()
423     {
424         return this.exist;
425     }
426 
427     /**
428      * Getter method for instance variable {@link #generalID}.
429      *
430      * @return value of instance variable {@link #generalID}
431      */
432     protected Long getGeneralID()
433     {
434         return this.generalID;
435     }
436 
437     /**
438      * Getter method for instance variable {@link #storeEvent}.
439      *
440      * @return value of instance variable {@link #storeEvent}
441      */
442     protected StoreEvent getStoreEvent()
443     {
444         return this.storeEvent;
445     }
446 
447     /**
448      * Wraps the standard {@link InputStream} to get an input stream for the needs of eFaps.
449      */
450     protected class StoreResourceInputStream
451         extends InputStream
452     {
453         /**
454          * InputStream.
455          */
456         private final InputStream in;
457 
458         /**
459          * StoreResource for this InputStream.
460          */
461         private final AbstractStoreResource store;
462 
463         /**
464          * @param _resource StoreResource this InputStream belong to
465          * @param _in       inputstream
466          * @throws IOException on error
467          */
468         protected StoreResourceInputStream(final AbstractStoreResource _resource,
469                                            final InputStream _in)
470             throws IOException
471         {
472             this.store = _resource;
473             if (_resource.getCompress().equals(Compress.GZIP)) {
474                 this.in = new GZIPInputStream(_in);
475             } else if (_resource.getCompress().equals(Compress.ZIP)) {
476                 this.in = new ZipInputStream(_in);
477             } else {
478                 this.in = _in;
479             }
480         }
481 
482         /**
483          * The input stream itself is closed.
484          *
485          * @throws IOException on error
486          */
487         protected void beforeClose()
488             throws IOException
489         {
490             if (this.in != null) {
491                 this.in.close();
492             }
493         }
494 
495         /**
496          * Only a dummy method if something must happened after the commit of the store.
497          *
498          * @throws IOException on error
499          */
500         protected void afterClose()
501             throws IOException
502         {
503         }
504 
505         /**
506          * The input stream and others are closes without commit of the store resource.
507          *
508          * @throws IOException on error
509          */
510         private void closeWithoutCommit()
511             throws IOException
512         {
513             beforeClose();
514             afterClose();
515         }
516 
517         /**
518          * Calls method {@link #beforeClose()}, then commits the store and at least calls method {@link #afterClose()}.
519          *
520          * @see #beforeClose()
521          * @see #afterClose()
522          * @throws IOException on error
523          */
524         @Override
525         public void close()
526             throws IOException
527         {
528             try {
529                 super.close();
530                 beforeClose();
531                 if (this.store.isOpened()) {
532                     this.store.commit();
533                 }
534                 afterClose();
535             } catch (final EFapsException e) {
536                 throw new IOException("commit of store not possible", e);
537             } finally {
538                 if (this.store.isOpened()) {
539                     try {
540                         this.store.abort();
541                     } catch (final EFapsException e) {
542                         throw new IOException("store resource could not be aborted", e);
543                     }
544                 }
545             }
546         }
547 
548         /**
549          * @return 0 if available, else 1
550          * @throws IOException on error
551          * @see InputStream#available()
552          */
553         @Override
554         public int available()
555             throws IOException
556         {
557             return this.in == null ? 1 : this.in.available();
558         }
559 
560         /**
561          * @param _readlimit limit to read
562          * @see InputStream#mark(int)
563          */
564         @Override
565         public void mark(final int _readlimit)
566         {
567             this.in.mark(_readlimit);
568         }
569 
570         /**
571          * @return mark suported
572          * @see InputStream#markSupported()
573          */
574         @Override
575         public boolean markSupported()
576         {
577             return this.in.markSupported();
578         }
579 
580         /**
581          * @return readed
582          * @throws IOException on error
583          * @see InputStream#read()
584          */
585         @Override
586         public int read()
587             throws IOException
588         {
589             return this.in.read();
590         }
591 
592         /**
593          * @param _b byte to read
594          * @return     the total number of bytes read into the buffer, or
595          *             <code>-1</code> if there is no more data because the end of
596          *             the stream has been reached.
597          * @throws IOException on error
598          * @see InputStream#read(byte[])
599          */
600         @Override
601         public int read(final byte[] _b)
602             throws IOException
603         {
604             return this.in == null ? -1 : this.in.read(_b);
605         }
606 
607         /**
608          * @param _b byte
609          * @param _off offset
610          * @param _len length
611          * @return     the total number of bytes read into the buffer, or
612          *             <code>-1</code> if there is no more data because the end of
613          *             the stream has been reached.
614          * @throws IOException on error
615          * @see InputStream#read(byte[], int, int)
616          */
617         @Override
618         public int read(final byte[] _b,
619                         final int _off,
620                         final int _len)
621             throws IOException
622         {
623             return this.in == null ? -1 : this.in.read(_b, _off, _len);
624         }
625 
626         /**
627          * @throws IOException on error
628          * @see InputStream#reset()
629          */
630         @Override
631         public void reset()
632             throws IOException
633         {
634             if (this.in != null) {
635                 this.in.reset();
636             }
637         }
638 
639         /**
640          * @param _n n to skip
641          * @return the actual number of bytes skipped.
642          * @throws IOException on error
643          * @see InputStream#skip(long)
644          */
645         @Override
646         public long skip(final long _n)
647             throws IOException
648         {
649             return this.in == null ? 0 : this.in.skip(_n);
650         }
651     }
652 }