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.util.zip.GZIPOutputStream;
27  import java.util.zip.ZipOutputStream;
28  
29  import javax.naming.Context;
30  import javax.naming.InitialContext;
31  import javax.naming.NameClassPair;
32  import javax.naming.NamingEnumeration;
33  import javax.naming.NamingException;
34  import javax.transaction.xa.XAException;
35  import javax.transaction.xa.Xid;
36  
37  import org.apache.commons.vfs2.FileContent;
38  import org.apache.commons.vfs2.FileObject;
39  import org.apache.commons.vfs2.FileSystemException;
40  import org.apache.commons.vfs2.impl.DefaultFileSystemManager;
41  import org.apache.commons.vfs2.provider.FileProvider;
42  import org.efaps.db.Instance;
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   * <p>The class implements the {@link Resource} interface for Apache Jakarta
50   * Commons Virtual File System.<p/>
51   * <p>
52   * All different virtual file systems could be used. The algorithm is:
53   * <ol>
54   *   <li>check if the file already exists</li>
55   *   <li></li>
56   *   <li></li>
57   * </ol>
58   * The store implements the compress property setting on the type for
59   * <code>ZIP</code> and <code>GZIP</code>.</p>
60   *
61   * For each file id a new VFS store resource must be created.
62   *
63   * @author The eFaps Team
64   * @version $Id$
65   */
66  public class VFSStoreResource
67      extends AbstractStoreResource
68  {
69      /**
70       * Extension of the temporary file in the store used in the transaction
71       * that the original file is not overwritten.
72       */
73      private static final String EXTENSION_TEMP = ".tmp";
74  
75      /**
76       * Extension of a file in the store.
77       */
78      private static final String EXTENSION_NORMAL = "";
79  
80      /**
81       * Extension of a bakup file in the store.
82       */
83      private static final String EXTENSION_BACKUP = ".bak";
84  
85      /**
86       * Property Name of the number of sub directories.
87       */
88      private static final String PROPERTY_NUMBER_SUBDIRS = "VFSNumberSubDirectories";
89  
90      /**
91       * Property Name to define if the type if is used to define a sub
92       * directory.
93       */
94      private static final String PROPERTY_USE_TYPE = "VFSUseTypeIdInPath";
95  
96      /**
97       * Property Name to define if the type if is used to define a sub
98       * directory.
99       */
100     private static final String PROPERTY_NUMBER_BACKUP = "VFSNumberBackups";
101 
102     /**
103      * Property Name to define the base name.
104      */
105     private static final String PROPERTY_BASENAME = "VFSBaseName";
106 
107     /**
108      * Property Name for the class name of the Provider.
109      */
110     private static final String PROPERTY_PROVIDER = "VFSProvider";
111 
112     /**
113      * Logging instance used in this class.
114      */
115     private static final Logger LOG  = LoggerFactory.getLogger(VFSStoreResource.class);
116 
117     /**
118      * Buffer used to copy from the input stream to the output stream.
119      *
120      * @see #write(InputStream, int)
121      */
122     private final byte[] buffer = new byte[1024];
123 
124     /**
125      * Stores the name of the file including the correct directory.
126      */
127     private String storeFileName = null;
128 
129     /**
130      * FilesystemManager for this VFSStoreResource.
131      */
132     private DefaultFileSystemManager manager;
133 
134     /**
135      * NUmber of backup files to be kept.
136      */
137     private int numberBackup = 1;
138 
139 
140     /**
141      * Method called to initialize this StoreResource.
142      * @param _instance     Instance of the object this StoreResource is wanted
143      *                      for
144      * @param _store        Store this resource belongs to
145      * @throws EFapsException on error
146      * @see Resource#initialize(Instance, Map, Compress)
147      */
148     @Override
149     public void initialize(final Instance _instance,
150                            final Store _store)
151         throws EFapsException
152     {
153         super.initialize(_instance, _store);
154 
155         final StringBuilder fileNameTmp = new StringBuilder();
156 
157         final String useTypeIdStr = getStore().getResourceProperties().get(VFSStoreResource.PROPERTY_USE_TYPE);
158         if ("true".equalsIgnoreCase(useTypeIdStr))  {
159             fileNameTmp.append(getInstance().getType().getId()).append("/");
160         }
161 
162         final String numberSubDirsStr =  getStore().getResourceProperties().get(
163                         VFSStoreResource.PROPERTY_NUMBER_SUBDIRS);
164         if (numberSubDirsStr != null)  {
165             final long numberSubDirs = Long.parseLong(numberSubDirsStr);
166             final String pathFormat = "%0"
167                           + Math.round(Math.log10(numberSubDirs) + 0.5d)
168                           + "d";
169             fileNameTmp.append(String.format(pathFormat,
170                             getInstance().getId() % numberSubDirs))
171                    .append("/");
172         }
173         fileNameTmp.append(getInstance().getType().getId()).append(".").append(getInstance().getId());
174         this.storeFileName = fileNameTmp.toString();
175 
176         final String numberBackupStr = getStore().getResourceProperties().get(VFSStoreResource.PROPERTY_NUMBER_BACKUP);
177         if (numberBackupStr != null) {
178             this.numberBackup  = Integer.parseInt(numberBackupStr);
179         }
180 
181         if (this.manager == null) {
182             try {
183                 DefaultFileSystemManager tmpMan = null;
184                 if (getStore().getResourceProperties().containsKey(Store.PROPERTY_JNDINAME)) {
185                     final InitialContext initialContext = new InitialContext();
186                     final Context context = (Context) initialContext.lookup("java:comp/env");
187                     final NamingEnumeration<NameClassPair> nameEnum = context.list("");
188                     while (nameEnum.hasMoreElements()) {
189                         final NameClassPair namePair = nameEnum.next();
190                         if (namePair.getName().equals(getStore().getResourceProperties().get(
191                                         Store.PROPERTY_JNDINAME))) {
192                             tmpMan = (DefaultFileSystemManager) context.lookup(
193                                             getStore().getResourceProperties().get(Store.PROPERTY_JNDINAME));
194                             break;
195                         }
196                     }
197                 }
198                 if (tmpMan == null && this.manager == null) {
199                     this.manager = evaluateFileSystemManager();
200                 }
201             } catch (final NamingException e) {
202                 throw new EFapsException(VFSStoreResource.class, "initialize.NamingException", e);
203             }
204         }
205     }
206 
207     /**
208      * @return DefaultFileSystemManager
209      * @throws EFapsException on error
210      */
211     private DefaultFileSystemManager evaluateFileSystemManager()
212         throws EFapsException
213     {
214         final DefaultFileSystemManager ret = new DefaultFileSystemManager();
215 
216         final String baseName =  getProperties().get(VFSStoreResource.PROPERTY_BASENAME);
217         final String provider =  getProperties().get(VFSStoreResource.PROPERTY_PROVIDER);
218         try {
219             ret.init();
220             final FileProvider fileProvider = (FileProvider) Class.forName(provider).newInstance();
221             ret.addProvider(baseName, fileProvider);
222             ret.setBaseFile(fileProvider.findFile(null, baseName, null));
223         } catch (final FileSystemException e) {
224             throw new EFapsException(VFSStoreResource.class,
225                                      "evaluateFileSystemManager.FileSystemException",
226                                      e, provider, baseName);
227         } catch (final InstantiationException e) {
228             throw new EFapsException(VFSStoreResource.class,
229                                      "evaluateFileSystemManager.InstantiationException",
230                                      e, baseName, provider);
231         } catch (final IllegalAccessException e) {
232             throw new EFapsException(VFSStoreResource.class,
233                                      "evaluateFileSystemManager.IllegalAccessException",
234                                      e, provider);
235         } catch (final ClassNotFoundException e) {
236             throw new EFapsException(VFSStoreResource.class,
237                                      "evaluateFileSystemManager.ClassNotFoundException",
238                                      e, provider);
239         }
240         return ret;
241     }
242 
243     /**
244      * {@inheritDoc}
245      */
246     @Override
247     protected int add2Select(final SQLSelect _select)
248     {
249         return 0;
250     }
251 
252     /**
253      * The method writes the context (from the input stream) to a temporary file
254      * (same file URL, but with extension {@link #EXTENSION_TEMP}).
255      *
256      * @param _in   input stream defined the content of the file
257      * @param _size length of the content (or negative meaning that the length
258      *              is not known; then the content gets the length of readable
259      *              bytes from the input stream)
260      * @param _fileName name of the file
261      * @return size of the created temporary file object
262      * @throws EFapsException on error
263      */
264     public long write(final InputStream _in,
265                       final long _size,
266                       final String _fileName)
267         throws EFapsException
268     {
269         try  {
270             long size = _size;
271             final FileObject tmpFile = this.manager.resolveFile(this.manager.getBaseFile(),
272                                             this.storeFileName + VFSStoreResource.EXTENSION_TEMP);
273             if (!tmpFile.exists()) {
274                 tmpFile.createFile();
275             }
276             final FileContent content = tmpFile.getContent();
277             OutputStream out = content.getOutputStream(false);
278             if (getCompress().equals(Compress.GZIP))  {
279                 out = new GZIPOutputStream(out);
280             } else if (getCompress().equals(Compress.ZIP))  {
281                 out = new ZipOutputStream(out);
282             }
283 
284             // if size is unkown!
285             if (_size < 0)  {
286                 int length = 1;
287                 size = 0;
288                 while (length > 0)  {
289                     length = _in.read(this.buffer);
290                     if (length > 0)  {
291                         out.write(this.buffer, 0, length);
292                         size += length;
293                     }
294                 }
295             } else  {
296                 Long length = _size;
297                 while (length > 0) {
298                     final int readLength = length.intValue() < this.buffer.length
299                                     ? length.intValue()  : this.buffer.length;
300                     _in.read(this.buffer, 0, readLength);
301                     out.write(this.buffer, 0, readLength);
302                     length -= readLength;
303                 }
304             }
305             if (getCompress().equals(Compress.GZIP) || getCompress().equals(Compress.ZIP))  {
306                 out.close();
307             }
308             tmpFile.close();
309             setFileInfo(_fileName, size);
310             return size;
311         } catch (final IOException e)  {
312             VFSStoreResource.LOG.error("write of content failed", e);
313             throw new EFapsException(VFSStoreResource.class, "write.IOException", e);
314         }
315 
316     }
317 
318     /**
319      * Deletes the file defined in {@link #fileId}.
320      */
321     public void delete()
322     {
323         //Deletion is done on commit
324     }
325 
326     /**
327      * Returns for the file the input stream.
328      *
329      * @return input stream of the file with the content
330      * @throws EFapsException on error
331      */
332     public InputStream read()
333         throws EFapsException
334     {
335         StoreResourceInputStream in = null;
336         try  {
337             final FileObject file = this.manager.resolveFile(this.storeFileName + VFSStoreResource.EXTENSION_NORMAL);
338             if (!file.isReadable())  {
339                 VFSStoreResource.LOG.error("file for " + this.storeFileName + " not readable");
340                 throw new EFapsException(VFSStoreResource.class, "#####file not readable");
341             }
342             in = new VFSStoreResourceInputStream(this, file);
343         } catch (final FileSystemException e)  {
344             VFSStoreResource.LOG.error("read of " + this.storeFileName + " failed", e);
345             throw new EFapsException(VFSStoreResource.class, "read.Throwable", e);
346         } catch (final IOException e) {
347             VFSStoreResource.LOG.error("read of " + this.storeFileName + " failed", e);
348             throw new EFapsException(VFSStoreResource.class, "read.Throwable", e);
349         }
350         return in;
351     }
352 
353     /**
354      * Ask the resource manager to prepare for a transaction commit of the
355      * transaction specified in xid (used for 2-phase commits).
356      *
357      * @param _xid Xid
358      * @return always 0
359      */
360     public int prepare(final Xid _xid)
361     {
362         if (VFSStoreResource.LOG.isDebugEnabled())  {
363             VFSStoreResource.LOG.debug("prepare (xid=" + _xid + ")");
364         }
365         return 0;
366     }
367 
368     /**
369      * Method that deletes the oldest backup and moves the others one up.
370      *
371      * @param _backup   file to backup
372      * @param _number         number of backup
373      * @throws FileSystemException on error
374      */
375     private void backup(final FileObject _backup,
376                         final int _number)
377         throws FileSystemException
378     {
379         if (_number < this.numberBackup) {
380             final FileObject backFile = this.manager.resolveFile(this.manager.getBaseFile(),
381                     this.storeFileName + VFSStoreResource.EXTENSION_BACKUP + _number);
382             if (backFile.exists()) {
383                 backup(backFile, _number + 1);
384             }
385             _backup.moveTo(backFile);
386         } else {
387             _backup.delete();
388         }
389     }
390 
391     /**
392      * The method is called from the transaction manager if the complete
393      * transaction is completed.<br/>
394      * A file in the virtual file system is committed with the algorithms:
395      * <ol>
396      * <li>any existing backup fill will be moved to an older backup file. The
397      *     maximum number of backups can be defined by setting the property
398      *     {@link #PROPERTY_NUMBER_BACKUP}. Default is one. To disable the
399      *     property must be set to 0.</li>
400      * <li>the current file is moved to the backup file (or deleted if property
401      *     {@link #PROPERTY_NUMBER_BACKUP} is 0)</li>
402      * <li>the new file is moved to the original name</li>
403      * </ol>
404      *
405      * @param _xid      global transaction identifier (not used, because each
406      *                  file with the file id gets a new VFS store resource
407      *                  instance)
408      * @param _onePhase <i>true</i> if it is a one phase commitment transaction
409      *                  (not used)
410      * @throws XAException if any exception occurs (catch on
411      *         {@link java.lang.Throwable})
412      */
413     public void commit(final Xid _xid,
414                        final boolean _onePhase)
415         throws XAException
416     {
417         if (VFSStoreResource.LOG.isDebugEnabled())  {
418             VFSStoreResource.LOG.debug("transaction commit");
419         }
420         if (getStoreEvent() == VFSStoreResource.StoreEvent.WRITE) {
421             try {
422                 final FileObject tmpFile = this.manager.resolveFile(this.manager.getBaseFile(),
423                         this.storeFileName + VFSStoreResource.EXTENSION_TEMP);
424                 final FileObject currentFile = this.manager.resolveFile(this.manager.getBaseFile(),
425                         this.storeFileName + VFSStoreResource.EXTENSION_NORMAL);
426                 final FileObject bakFile = this.manager.resolveFile(this.manager.getBaseFile(),
427                         this.storeFileName + VFSStoreResource.EXTENSION_BACKUP);
428                 if (bakFile.exists() && (this.numberBackup > 0)) {
429                     backup(bakFile, 0);
430                 }
431                 if (currentFile.exists()) {
432                     if (this.numberBackup > 0) {
433                         currentFile.moveTo(bakFile);
434                     } else {
435                         currentFile.delete();
436                     }
437                 }
438                 tmpFile.moveTo(currentFile);
439                 tmpFile.close();
440                 currentFile.close();
441                 bakFile.close();
442             } catch (final FileSystemException e)  {
443                 VFSStoreResource.LOG.error("transaction commit fails for " + _xid
444                         + " (one phase = " + _onePhase + ")", e);
445                 final XAException xa = new XAException(XAException.XA_RBCOMMFAIL);
446                 xa.initCause(e);
447                 throw xa;
448             }
449         } else if (getStoreEvent() == VFSStoreResource.StoreEvent.DELETE) {
450             try {
451                 final FileObject curFile = this.manager.resolveFile(this.manager.getBaseFile(),
452                             this.storeFileName + VFSStoreResource.EXTENSION_NORMAL);
453                 final FileObject bakFile = this.manager.resolveFile(this.manager.getBaseFile(),
454                         this.storeFileName + VFSStoreResource.EXTENSION_BACKUP);
455                 if (bakFile.exists()) {
456                     bakFile.delete();
457                 }
458                 if (curFile.exists())  {
459                     curFile.moveTo(bakFile);
460                 }
461                 bakFile.close();
462                 curFile.close();
463             } catch (final FileSystemException e) {
464                 VFSStoreResource.LOG.error("transaction commit fails for " + _xid
465                                 + " (one phase = " + _onePhase + ")", e);
466                 final XAException xa = new XAException(XAException.XA_RBCOMMFAIL);
467                 xa.initCause(e);
468                 throw xa;
469             }
470         }
471     }
472 
473     /**
474      * If the file written in the virtual file system must be rolled back, only
475      * the created temporary file (created from method {@link #write}) is
476      * deleted.
477      *
478      * @param _xid      global transaction identifier (not used, because each
479      *                  file with the file id gets a new VFS store resource
480      *                  instance)
481      * @throws XAException if any exception occurs (catch on
482      *         {@link java.lang.Throwable})
483      */
484     public void rollback(final Xid _xid)
485         throws XAException
486     {
487         if (VFSStoreResource.LOG.isDebugEnabled())  {
488             VFSStoreResource.LOG.debug("rollback (xid = " + _xid + ")");
489         }
490         try {
491             final FileObject tmpFile = this.manager.resolveFile(this.manager.getBaseFile(),
492                     this.storeFileName + VFSStoreResource.EXTENSION_TEMP);
493             if (tmpFile.exists()) {
494                 tmpFile.delete();
495             }
496         } catch (final FileSystemException e)  {
497             VFSStoreResource.LOG.error("transaction rollback fails for " + _xid, e);
498             final XAException xa = new XAException(XAException.XA_RBCOMMFAIL);
499             xa.initCause(e);
500             throw xa;
501         }
502     }
503 
504     /**
505      * Tells the resource manager to forget about a heuristically completed
506      * transaction branch.
507      *
508      * @param _xid  global transaction identifier (not used, because each file
509      *              with the file id gets a new VFS store resource instance)
510      */
511     public void forget(final Xid _xid)
512     {
513         if (VFSStoreResource.LOG.isDebugEnabled()) {
514             VFSStoreResource.LOG.debug("forget (xid = " + _xid + ")");
515         }
516     }
517 
518     /**
519      * Obtains the current transaction timeout value set for this XAResource
520      * instance.
521      *
522      * @return always 0
523      */
524     public int getTransactionTimeout()
525     {
526         if (VFSStoreResource.LOG.isDebugEnabled()) {
527             VFSStoreResource.LOG.debug("getTransactionTimeout");
528         }
529         return 0;
530     }
531 
532     /**
533      * Obtains a list of prepared transaction branches from a resource manager.
534      *
535      * @param _flag flag
536      * @return always <code>null</code>
537      */
538     public Xid[] recover(final int _flag)
539     {
540         if (VFSStoreResource.LOG.isDebugEnabled()) {
541             VFSStoreResource.LOG.debug("recover (flag = " + _flag + ")");
542         }
543         return null;
544     }
545 
546     /**
547      * Sets the current transaction timeout value for this XAResource instance.
548      *
549      * @param _seconds number of seconds
550      * @return always <i>true</i>
551      */
552     public boolean setTransactionTimeout(final int _seconds)
553     {
554         if (VFSStoreResource.LOG.isDebugEnabled()) {
555             VFSStoreResource.LOG.debug("setTransactionTimeout (seconds = " + _seconds + ")");
556         }
557         return true;
558     }
559 
560     /**
561      * Input stream wrapper class.
562      */
563     private class VFSStoreResourceInputStream
564         extends StoreResourceInputStream
565     {
566         /**
567          * File to be stored.
568          */
569         private final FileObject file;
570 
571         /**
572          * @param _storeRes   storeresource
573          * @param _file       file to store
574          * @throws IOException on error
575          */
576         protected VFSStoreResourceInputStream(final AbstractStoreResource _storeRes,
577                                               final FileObject _file)
578             throws IOException
579         {
580             super(_storeRes, _file.getContent().getInputStream());
581             this.file = _file;
582         }
583 
584         /**
585          * The file object {@link #file} is closed. The method overwrites the
586          * method to close the input stream, because if the file is closed, the
587          * input stream is also closed.
588          *
589          * @throws IOException on error
590          */
591         @Override
592         protected void beforeClose() throws IOException
593         {
594             this.file.close();
595         }
596     }
597 }