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.ByteArrayOutputStream;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.OutputStream;
27  import java.sql.Connection;
28  import java.sql.PreparedStatement;
29  import java.sql.ResultSet;
30  import java.sql.SQLException;
31  import java.util.Calendar;
32  
33  import javax.jcr.AccessDeniedException;
34  import javax.jcr.Binary;
35  import javax.jcr.InvalidItemStateException;
36  import javax.jcr.ItemExistsException;
37  import javax.jcr.LoginException;
38  import javax.jcr.NoSuchWorkspaceException;
39  import javax.jcr.Node;
40  import javax.jcr.Property;
41  import javax.jcr.ReferentialIntegrityException;
42  import javax.jcr.Repository;
43  import javax.jcr.RepositoryException;
44  import javax.jcr.Session;
45  import javax.jcr.SimpleCredentials;
46  import javax.jcr.lock.LockException;
47  import javax.jcr.nodetype.ConstraintViolationException;
48  import javax.jcr.nodetype.NoSuchNodeTypeException;
49  import javax.jcr.nodetype.NodeType;
50  import javax.jcr.version.VersionException;
51  import javax.naming.InitialContext;
52  import javax.naming.NamingException;
53  import javax.transaction.xa.XAException;
54  import javax.transaction.xa.Xid;
55  
56  import org.efaps.db.Context;
57  import org.efaps.db.Instance;
58  import org.efaps.db.transaction.ConnectionResource;
59  import org.efaps.db.wrapper.SQLSelect;
60  import org.efaps.util.EFapsException;
61  import org.slf4j.Logger;
62  import org.slf4j.LoggerFactory;
63  
64  /**
65   * Store Resource that uses the Content Repository for Java Technology API (JCR).
66   *
67   * @author The eFaps Team
68   * @version $Id$
69   */
70  public class JCRStoreResource
71      extends AbstractStoreResource
72  {
73      /**
74       * Name of the table the content is stored in.
75       */
76      public static final String TABLENAME_STORE = "T_CMGENSTOREJCR";
77  
78      /**
79       * Name of the column the content is stored in.
80       */
81      public static final String COLNAME_IDENTIFIER = "IDENTIFIER";
82  
83      /**
84       * Logging instance used in this class.
85       */
86      private static final Logger LOG  = LoggerFactory.getLogger(JCRStoreResource.class);
87  
88      /**
89       * Property Name to define if the type if is used to define a sub
90       * directory.
91       */
92      private static final String PROPERTY_WORKSPACENAME = "JCRWorkSpaceName";
93  
94      /**
95       * Property Name to define if the file will be deleted on deletion of the related object.
96       */
97      private static final String PROPERTY_ENABLEDELETION = "JCREnableDeletion";
98  
99      /**
100      * Property Name to define if the file will be deleted on deletion of the related object.
101      */
102     private static final String PROPERTY_USERNAME = "JCRUserName";
103 
104     /**
105      * Property Name to define if the file will be deleted on deletion of the related object.
106      */
107     private static final String PROPERTY_PASSWORD = "JCRPassword";
108 
109 
110     /**
111      * The repository for this JCR Store Resource.
112      */
113     private Repository repository;
114 
115     /**
116      * Identifier for the node to be accessed.
117      */
118     private String identifier;
119 
120     /**
121      * Session for JCR access.
122      */
123     private Session session;
124 
125     /**
126      * {@inheritDoc}
127      */
128     @Override
129     public void initialize(final Instance _instance,
130                            final Store _store)
131         throws EFapsException
132     {
133         super.initialize(_instance, _store);
134         try {
135             final InitialContext ctx = new InitialContext();
136             this.repository = (Repository) ctx.lookup(getStore().getProperty(Store.PROPERTY_JNDINAME));
137             if (JCRStoreResource.LOG.isDebugEnabled()) {
138                 final String name = this.repository.getDescriptor(Repository.REP_NAME_DESC);
139                 JCRStoreResource.LOG.debug("Successfully retrieved '%s' repository from JNDI", new Object[]{ name });
140             }
141             String username = getProperties().get(JCRStoreResource.PROPERTY_USERNAME);
142             if (username == null) {
143                 username = Context.getThreadContext().getPerson().getName();
144             }
145             String passwd = getProperties().get(JCRStoreResource.PROPERTY_PASSWORD);
146             if (passwd == null) {
147                 passwd = "efaps";
148             }
149             this.session = this.repository.login(new SimpleCredentials(username, passwd.toCharArray()),
150                             getProperties().get(JCRStoreResource.PROPERTY_WORKSPACENAME));
151         } catch (final NamingException e) {
152             throw new EFapsException(JCRStoreResource.class, "initialize.NamingException", e);
153         } catch (final LoginException e) {
154             throw new EFapsException(JCRStoreResource.class, "initialize.LoginException", e);
155         } catch (final NoSuchWorkspaceException e) {
156             throw new EFapsException(JCRStoreResource.class, "initialize.NoSuchWorkspaceException", e);
157         } catch (final RepositoryException e) {
158             throw new EFapsException(JCRStoreResource.class, "initialize.RepositoryException", e);
159         }
160     }
161 
162     /**
163      * {@inheritDoc}
164      */
165     @Override
166     protected int add2Select(final SQLSelect _select)
167     {
168         _select.column(2, "ID").column(2, JCRStoreResource.COLNAME_IDENTIFIER)
169             .leftJoin(JCRStoreResource.TABLENAME_STORE, 2, "ID", 0, "ID");
170         return 1;
171     }
172 
173     /**
174      * {@inheritDoc}
175      */
176     @Override
177     protected void getAdditionalInfo(final ResultSet _rs)
178         throws SQLException
179     {
180         final String identiferTmp = _rs.getString(6);
181         if (identiferTmp != null && !identiferTmp.isEmpty()) {
182             this.identifier = identiferTmp.trim();
183         }
184     }
185 
186     /**
187      * {@inheritDoc}
188      */
189     @Override
190     protected void insertDefaults()
191         throws EFapsException
192     {
193         super.insertDefaults();
194         if (!getExist()[1] && getGeneralID() != null) {
195             try {
196                 final ConnectionResource res = Context.getThreadContext().getConnectionResource();
197                 final Connection con = res.getConnection();
198                 Context.getDbType().newInsert(JCRStoreResource.TABLENAME_STORE, "ID", false)
199                                 .column("ID", getGeneralID())
200                                 .column(JCRStoreResource.COLNAME_IDENTIFIER, "NEW")
201                                 .execute(con);
202                 res.commit();
203             } catch (final SQLException e) {
204                 throw new EFapsException(JCRStoreResource.class, "insertDefaults", e);
205             }
206         }
207     }
208 
209     /**
210      * {@inheritDoc}
211      */
212     @Override
213     public long write(final InputStream _in,
214                      final long _size,
215                      final String _fileName)
216         throws EFapsException
217     {
218         final JCRBinary bin = new JCRBinary(_in);
219         long size = _size;
220         try {
221             final Node rootNode = this.session.getRootNode();
222             final Node fileNode = rootNode.addNode(getInstance().getOid(), NodeType.NT_FILE);
223             final Node resNode = fileNode.addNode(Property.JCR_CONTENT, NodeType.NT_RESOURCE);
224             resNode.setProperty(Property.JCR_DATA, bin);
225             resNode.setProperty(Property.JCR_LAST_MODIFIED, Calendar.getInstance());
226             resNode.setProperty(Property.JCR_LAST_MODIFIED_BY, Context.getThreadContext().getPerson().getName());
227             setIdentifer(fileNode.getIdentifier());
228             // if size is unkown!
229             if (size < 0)  {
230                 final byte[] buffer = new byte[1024];
231                 int length = 1;
232                 size = 0;
233                 final OutputStream out = new ByteArrayOutputStream();
234                 while (length > 0)  {
235                     length = _in.read(buffer);
236                     if (length > 0)  {
237                         out.write(buffer, 0, length);
238                         size += length;
239                     }
240                 }
241             }
242         } catch (final RepositoryException e) {
243             throw new EFapsException(JCRStoreResource.class, "write.RepositoryException", e);
244         } catch (final IOException e) {
245             throw new EFapsException(JCRStoreResource.class, "write.IOException", e);
246         }
247         setFileInfo(_fileName, size);
248         return size;
249     }
250 
251     /**
252      * Set the identifier in the eFaps DataBase.
253      * @param _identifier   identifer to set
254      * @throws EFapsException on error
255      */
256     protected void setIdentifer(final String _identifier)
257         throws EFapsException
258     {
259         if (!_identifier.equals(this.identifier)) {
260 
261             ConnectionResource res = null;
262             try {
263                 res = Context.getThreadContext().getConnectionResource();
264 
265                 final StringBuffer cmd = new StringBuffer().append("update ")
266                                 .append(JCRStoreResource.TABLENAME_STORE).append(" set ")
267                                 .append(JCRStoreResource.COLNAME_IDENTIFIER).append("=? ")
268                                 .append("where ID =").append(getGeneralID());
269 
270                 final PreparedStatement stmt = res.getConnection().prepareStatement(cmd.toString());
271                 try {
272                     stmt.setString(1, _identifier);
273                     stmt.execute();
274                 } finally {
275                     stmt.close();
276                 }
277                 res.commit();
278                 this.identifier = _identifier;
279             } catch (final EFapsException e) {
280                 res.abort();
281                 throw e;
282             } catch (final SQLException e) {
283                 res.abort();
284                 throw new EFapsException(JDBCStoreResource.class, "write.SQLException", e);
285             }
286         }
287     }
288 
289     /**
290      * A JCR Store resource does not use compression from eFaps Side.
291      * @return Compress.NONE
292      */
293     @Override
294     protected Compress getCompress()
295     {
296         return Compress.NONE;
297     }
298 
299     /**
300      * {@inheritDoc}
301      */
302     @Override
303     public InputStream read()
304         throws EFapsException
305     {
306         InputStream input = null;
307         try {
308             final Node fileNode = this.session.getNodeByIdentifier(this.identifier);
309             final Node resNode = fileNode.getNode(Property.JCR_CONTENT);
310             final Property data = resNode.getProperty(Property.JCR_DATA);
311             final Binary bin = data.getBinary();
312             input = new JCRStoreResourceInputStream(this, bin);
313         } catch (final RepositoryException e) {
314             throw new EFapsException(JCRStoreResource.class, "read.RepositoryException", e);
315         } catch (final IOException e) {
316             throw new EFapsException(JCRStoreResource.class, "read.IOException", e);
317         }
318         return input;
319     }
320 
321     /**
322      * {@inheritDoc}
323      */
324     @Override
325     public void delete()
326         throws EFapsException
327     {
328         // only delete if it actually exists and deletion is configured
329         if (getExist()[0] && getExist()[1]
330                         && "TRUE".equalsIgnoreCase(getProperties().get(JCRStoreResource.PROPERTY_ENABLEDELETION))) {
331             try {
332                 final Node fileNode = this.session.getNodeByIdentifier(this.identifier);
333                 fileNode.remove();
334             } catch (final RepositoryException e) {
335                 throw new EFapsException(JCRStoreResource.class, "delete.RepositoryException", e);
336             }
337         }
338     }
339 
340     /**
341      * The method is called from the transaction manager if the complete
342      * transaction is completed.<br/>
343      *
344      * @param _xid      global transaction identifier (not used, because each
345      *                  file with the file id gets a new VFS store resource
346      *                  instance)
347      * @param _onePhase <i>true</i> if it is a one phase commitment transaction
348      *                  (not used)
349      * @throws XAException if any exception occurs (catch on
350      *         {@link java.lang.Throwable})
351      */
352     @Override
353     public void commit(final Xid _xid,
354                        final boolean _onePhase)
355         throws XAException
356     {
357         try {
358             if (this.session.hasPendingChanges()) {
359                 this.session.save();
360             }
361             this.session.logout();
362         } catch (final AccessDeniedException e) {
363             throw new XAException("AccessDeniedException");
364         } catch (final ItemExistsException e) {
365             throw new XAException("ItemExistsException");
366         } catch (final ReferentialIntegrityException e) {
367             throw new XAException("ReferentialIntegrityException");
368         } catch (final ConstraintViolationException e) {
369             throw new XAException("AccessDeniedException");
370         } catch (final InvalidItemStateException e) {
371             throw new XAException("InvalidItemStateException");
372         } catch (final VersionException e) {
373             throw new XAException("VersionException");
374         } catch (final LockException e) {
375             throw new XAException(XAException.XA_RBDEADLOCK);
376         } catch (final NoSuchNodeTypeException e) {
377             throw new XAException("NoSuchNodeTypeException");
378         } catch (final RepositoryException e) {
379             throw new XAException("RepositoryException");
380         }
381     }
382 
383     /**
384      * Tells the resource manager to forget about a heuristically completed
385      * transaction branch.
386      *
387      * @param _xid  global transaction identifier (not used, because each file
388      *              with the file id gets a new VFS store resource instance)
389      */
390     @Override
391     public void forget(final Xid _xid)
392     {
393         if (JCRStoreResource.LOG.isDebugEnabled()) {
394             JCRStoreResource.LOG.debug("forget (xid = " + _xid + ")");
395         }
396     }
397 
398     /**
399      * Obtains the current transaction timeout value set for this XAResource
400      * instance.
401      *
402      * @return always 0
403      */
404     public int getTransactionTimeout()
405     {
406         if (JCRStoreResource.LOG.isDebugEnabled()) {
407             JCRStoreResource.LOG.debug("getTransactionTimeout");
408         }
409         return 0;
410     }
411 
412     /**
413      * Ask the resource manager to prepare for a transaction commit of the
414      * transaction specified in xid (used for 2-phase commits).
415      *
416      * @param _xid Xid
417      * @return always 0, because not 2 phase commit
418      */
419     public int prepare(final Xid _xid)
420     {
421         if (JCRStoreResource.LOG.isDebugEnabled())  {
422             JCRStoreResource.LOG.debug("prepare (xid=" + _xid + ")");
423         }
424         return 0;
425     }
426 
427     /**
428      * Obtains a list of prepared transaction branches from a resource manager.
429      *
430      * @param _flag flag
431      * @return always <code>null</code>
432      */
433     public Xid[] recover(final int _flag)
434     {
435         if (JCRStoreResource.LOG.isDebugEnabled()) {
436             JCRStoreResource.LOG.debug("recover (flag = " + _flag + ")");
437         }
438         return null;
439     }
440 
441     /**
442      * On rollback no save is send to the session..
443      *
444      * @param _xid      global transaction identifier (not used, because each
445      *                  file with the file id gets a new VFS store resource
446      *                  instance)
447      * @throws XAException if any exception occurs (catch on
448      *         {@link java.lang.Throwable})
449      */
450     @Override
451     public void rollback(final Xid _xid)
452         throws XAException
453     {
454         this.session.logout();
455     }
456 
457     /**
458      * Sets the current transaction timeout value for this XAResource instance.
459      *
460      * @param _seconds number of seconds
461      * @return always <i>true</i>
462      */
463     public boolean setTransactionTimeout(final int _seconds)
464     {
465         if (JCRStoreResource.LOG.isDebugEnabled()) {
466             JCRStoreResource.LOG.debug("setTransactionTimeout (seconds = " + _seconds + ")");
467         }
468         return true;
469     }
470 
471 
472     /**
473      * Implementation of Binary from JCR.
474      */
475     private static class JCRBinary
476         implements Binary
477     {
478         /**
479          * The InpuStrema this Binary belongs to.
480          */
481         private InputStream stream;
482 
483         /**
484          * @param _in InputStream
485          */
486         public JCRBinary(final InputStream _in)
487         {
488             this.stream = _in;
489         }
490 
491         /**
492          * Returns an {@link InputStream} representation of this value. Each call to
493          * <code>getStream()</code> returns a new stream. The API consumer is
494          * responsible for calling <code>close()</code> on the returned stream.
495          * <p>
496          * If {@link #dispose()} has been called on this <code>Binary</code>
497          * object, then this method will throw the runtime exception
498          * {@link java.lang.IllegalStateException}.
499          *
500          * @return A stream representation of this value.
501          * @throws RepositoryException if an error occurs.
502          */
503         @Override
504         public InputStream getStream()
505             throws RepositoryException
506         {
507             return this.stream;
508         }
509 
510         /**
511          * Reads successive bytes from the specified <code>position</code> in this
512          * <code>Binary</code> into the passed byte array until either the byte
513          * array is full or the end of the <code>Binary</code> is encountered.
514          * <p>
515          * If {@link #dispose()} has been called on this <code>Binary</code>
516          * object, then this method will throw the runtime exception
517          * {@link java.lang.IllegalStateException}.
518          *
519          * @param _b        the buffer into which the data is read.
520          * @param _position the position in this Binary from which to start reading
521          *                 bytes.
522          * @return the number of bytes read into the buffer, or -1 if there is no
523          *         more data because the end of the Binary has been reached.
524          * @throws IOException              if an I/O error occurs.
525          * @throws NullPointerException     if b is null.
526          * @throws IllegalArgumentException if offset is negative.
527          * @throws RepositoryException      if another error occurs.
528          */
529         @Override
530         public int read(final byte[] _b,
531                         final long _position)
532             throws IOException, RepositoryException
533         {
534             return this.stream.read(_b);
535         }
536 
537         /**
538          * Returns the size of this <code>Binary</code> value in bytes.
539          * <p>
540          * If {@link #dispose()} has been called on this <code>Binary</code>
541          * object, then this method will throw the runtime exception
542          * {@link java.lang.IllegalStateException}.
543          *
544          * @return the size of this value in bytes.
545          * @throws RepositoryException if an error occurs.
546          */
547         @Override
548         public long getSize()
549             throws RepositoryException
550         {
551             return 0;
552         }
553 
554         /**
555          * Releases all resources associated with this <code>Binary</code> object
556          * and informs the repository that these resources may now be reclaimed.
557          * An application should call this method when it is finished with the
558          * <code>Binary</code> object.
559          */
560         @Override
561         public void dispose()
562         {
563             try {
564                 this.stream.close();
565             } catch (final IOException e) {
566                 JCRStoreResource.LOG.error("Error on disposal of inpustream.", e);
567             }
568         }
569     }
570 
571     /**
572      * ResourceInputStream implementation.
573      */
574     private class JCRStoreResourceInputStream
575         extends StoreResourceInputStream
576     {
577 
578         /**
579          * @param _store    Strore this InputStream belongs to
580          * @param _bin      Binary
581          * @throws IOException on error
582          * @throws RepositoryException  on error
583          */
584         protected JCRStoreResourceInputStream(final AbstractStoreResource _store,
585                                               final Binary _bin)
586             throws IOException, RepositoryException
587         {
588             super(_store, _bin.getStream());
589         }
590     }
591 }