001    /*
002      Copyright (C) 2001-2003 Lionel Seinturier <Lionel.Seinturier@lip6.fr>
003    
004      This program is free software; you can redistribute it and/or modify
005      it under the terms of the GNU Lesser General Public License as
006      published by the Free Software Foundation; either version 2 of the
007      License, or (at your option) any later version.
008    
009      This program is distributed in the hope that it will be useful,
010      but WITHOUT ANY WARRANTY; without even the implied warranty of
011      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012      GNU Lesser General Public License for more details.
013    
014      You should have received a copy of the GNU Lesser General Public License
015      along with this program; if not, write to the Free Software
016      Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA */
017    
018    package org.objectweb.jac.aspects.distrans.persistence;
019    
020    import org.objectweb.jac.core.NameRepository;
021    import org.objectweb.jac.core.rtti.ClassItem;
022    import org.objectweb.jac.core.rtti.ClassRepository;
023    import org.objectweb.jac.core.rtti.FieldItem;
024    import org.objectweb.jac.util.Repository;
025    
026    import java.sql.Connection;
027    import java.sql.PreparedStatement;
028    import java.sql.ResultSet;
029    import java.sql.SQLException;
030    import java.sql.Statement;
031    import java.util.Collection;
032    import java.util.HashMap;
033    import java.util.Iterator;
034    import java.util.Map;
035    
036    import javax.transaction.HeuristicMixedException;
037    import javax.transaction.HeuristicRollbackException;
038    import javax.transaction.NotSupportedException;
039    import javax.transaction.RollbackException;
040    import javax.transaction.SystemException;
041    
042    import org.enhydra.jdbc.standard.StandardXADataSource;
043    
044    /**
045     * Basic transaction-enabled persistence storage.
046     *
047     * Data is stored in SQL tables.
048     * Each table contains object field values and owns one more attribute
049     * than its associated class contains field. The additional attribute
050     * stores the object name.
051     * 
052     * @author Lionel Seinturier <Lionel.Seinturier@lip6.fr>
053     * @version 1.0
054     */
055    public class SimpleDbPersistence implements PersistenceItf {
056    
057        /**
058         * The name of the attribute storing the persistent object name
059         * in SQL tables.
060         */
061        final static private String objectAttributeName = "_objname";
062    
063        
064        /**
065         * Create a SQL table to hold persistent data.
066         * Implements PersistenceItf#createStorageIfNeeded()
067         * 
068         * The table is not created if it already exists. This enables to reuse
069         * existing tables to map their content to JAC objects. However, this may
070         * cause problems if the schema is different than the expected one. In
071         * such a case, call createStorage instead.
072         * 
073         * @param className   the class name for which we want to create a table
074         * @param ds          the data source
075         */
076        public void initStorageIfNeeded( String className, StandardXADataSource ds ) {
077            try {
078                            _initStorageIfNeeded(className,ds);
079                    } catch (SQLException e) {
080                            e.printStackTrace();
081                            System.exit(1);
082                    }
083        }
084        
085        private void _initStorageIfNeeded( String className, StandardXADataSource ds )
086            throws SQLException {
087    
088            /**
089             * Check whether a table exists for each element of classNames.
090             * The table name is the class name where dots delimiting package
091             * names are replaced by underscores.
092             */
093            Connection connection = XAPoolCache.getConnection(ds);
094            
095            String tableName = className.replace('.','_');                
096            Statement st = connection.createStatement();
097            try {
098                st.executeQuery("SELECT * FROM "+tableName);
099            }
100            catch (SQLException sqle) {
101                /**
102                 * If the table does not exist, the expected behavior is
103                 * to throw a SQLException. In such a case, create the table.
104                 */
105                initStorage(className,connection);
106            }
107        }
108        
109        
110        /**
111         * Create a SQL table to hold persistent data.
112         * If the table exists, it is deleted first. 
113         * Implements PersistenceItf#createStorage()
114         * 
115         * @param className   the class name for which we want to create a table
116         * @param ds          the data source
117         */
118        public void initStorage( String className, StandardXADataSource ds ) {
119            try {
120                            _initStorage(className,ds);
121                    } catch (SQLException e) {
122                            e.printStackTrace();
123                            System.exit(1);
124                    }
125        }
126        
127        private void _initStorage( String className, StandardXADataSource ds )
128            throws SQLException {
129    
130            /**
131             * Check whether a table exists for each element of classNames.
132             */
133            Connection connection = XAPoolCache.getConnection(ds);
134            
135            /**
136             * The table name is the class name where dots delimiting package
137             * names are replaced by underscores.
138             */
139            String tableName = className.replace('.','_');                
140            Statement st = connection.createStatement();
141            try {
142                st.executeQuery("DROP TABLE "+tableName);
143            }
144            catch (SQLException sqle) {
145                /**
146                 * If the table does not exist, the expected behavior is
147                 * to throw a SQLException.
148                 */
149            }
150            
151            initStorage(className,connection);
152        }
153    
154        
155        /**
156         * Create a SQL table to hold persistent data.
157         * 
158         * @param className   the class name
159         * @param connection  the connection towards the database
160         */
161        private void initStorage( String className, Connection connection )
162            throws SQLException {
163            
164            /**
165             * The table name is the class name where dots delimiting package names
166             * are replaced by underscores.
167             */
168            String tableName = className.replace('.','_');
169                    
170            String sql =
171                "CREATE TABLE "+tableName+" ( "+
172                objectAttributeName+" VARCHAR PRIMARY KEY";
173                
174            ClassItem classItem = classes.getClass(className);
175            Collection fields = classItem.getAllFields();
176            for (Iterator iter = fields.iterator(); iter.hasNext();) {
177                FieldItem field = (FieldItem) iter.next();
178                String fieldName = field.getName();
179                String fieldType = field.getType().getName();
180                String sqlType = (String) SimpleDbPersistence.javaTypesToSQL.get(fieldType);
181                sql += ", "+fieldName+" "+sqlType;
182            }
183            sql += ");";
184            
185            Statement st = connection.createStatement();
186            st.executeUpdate(sql);
187            st.close();
188        }
189        
190        /**
191         * Store mappings between Java types and SQL ones.
192         * For now on, this is only a quick-and-dirty partial mapping.
193         * To be completed.
194         */
195        private static Map javaTypesToSQL = new HashMap();
196        static {
197            javaTypesToSQL.put("java.lang.String","VARCHAR");
198            javaTypesToSQL.put("int","INT");
199            javaTypesToSQL.put("double","DOUBLE PRECISION");
200        }
201        
202        
203        /**
204         * Store an object into the persistence storage.
205         * Implements PersistenceItf#load()
206         * 
207         * @param wrappee  the object to store
208         * @param name     the identifier for the object
209         * @param ds       the data source
210         */
211        public void load( Object wrappee, String name, StandardXADataSource ds )
212            throws SQLException, IllegalArgumentException, IllegalAccessException, NotSupportedException, SecurityException, IllegalStateException, RollbackException, HeuristicMixedException, HeuristicRollbackException, SystemException {
213                    
214            ClassItem classItem = classes.getClass(wrappee);
215            
216            /**
217             * Get a prepared statement to fetch the data.
218             * It may be cached in the selectStatements map.
219             */
220            Connection connection = XAPoolCache.getConnection(ds);
221    
222            PreparedStatement ps =
223                createSelectStatement(connection,classItem);        
224            ps.setString(1,name);
225            ResultSet rs = ps.executeQuery();
226            
227            /**
228             * The object hasn't been found in the storage.
229             * The most likely reason is that it has never been
230             * written before. In such a case, do nothing (ie don't
231             * reflect any value on the wrappee).
232             */
233            boolean found = rs.next();
234            if (!found)  return;
235            
236            /**
237             * Set fields with values retrieved from the ResultSet.
238             * Start at index 2 to skip the wrappeeName.
239             */
240            Collection fields = classItem.getAllFields();
241            int i=2;
242            for (Iterator iter = fields.iterator(); iter.hasNext();i++) {
243                FieldItem field = (FieldItem) iter.next();
244                Object value = rs.getObject(i);
245                field.set(wrappee,value);
246            }
247            rs.close();
248        }
249    
250        
251        /**
252         * Update an object with the values retrieved from the persistent
253         * storage.
254         * Implements PersistenceItf#store()
255         * 
256         * @param wrappee  the object to update
257         * @param name     the identifier for the object
258         * @param ds       the data source
259         */
260        public void store( Object wrappee, String name, StandardXADataSource ds )
261            throws Exception {
262    
263            ClassItem classItem = classes.getClass(wrappee);
264            
265            /**
266             * Save the wrappee field values into the database.
267             * Get a prepared statement to update the data.
268             * It may be cached in the updateStatements map.
269             */
270            Connection connection = XAPoolCache.getConnection(ds);
271            
272            PreparedStatement ps =
273                createUpdateStatement(connection,classItem);
274            
275            Collection fields = classItem.getAllFields();
276            int i=1;
277            for (Iterator iter = fields.iterator(); iter.hasNext();i++) {
278                FieldItem field = (FieldItem) iter.next();
279                Object value = field.get(wrappee);
280                ps.setObject(i,value);
281            }
282            ps.setString(i,name);
283            
284            /**
285             * Update the data in the table.
286             * If 0 row have been updates, the row didn't exist yet, create it.
287             */
288            int rowCount = ps.executeUpdate();
289            
290            if ( rowCount == 0 ) {
291                PreparedStatement insertps =
292                    createInsertStatement(
293                        connection,classItem,wrappee,name
294                    );
295                insertps.executeUpdate();
296                insertps.close();
297                ps.executeUpdate();
298            }
299        }
300    
301        /**
302         * Create an INSERT SQL request to store the field values of an object.
303         * Each field is mapped onto a SQL attribute.
304         * The table contains one more attribute which is the name of the wrappee,
305         * and which is also the primary key of the table.
306         * 
307         * @param connection   the connection towards the database
308         * @param cl           the class
309         * @param wrappee      the persistent object (ie the wrappee)
310         * @param wrappeeName  the wrappee name
311         * @return             the prepared statement
312         */
313        private static PreparedStatement createInsertStatement(
314            Connection connection, ClassItem cl,
315            Object wrappee, String wrappeeName ) throws SQLException {
316                
317            /**
318             * The table name is the class name where dots delimiting package names
319             * are replaced by underscores.
320             */
321            String tableName = cl.getName();
322            tableName = tableName.replace('.','_');
323            String request = "INSERT INTO "+tableName+" VALUES (?";
324        
325            Collection fields = cl.getAllFields();
326            for (Iterator iter = fields.iterator(); iter.hasNext();) {
327                iter.next();
328                request += ",?";
329            }
330        
331            request += ");";
332            
333            PreparedStatement ps = connection.prepareStatement(request);
334        
335            ps.setString(1,wrappeeName);
336            int i=2;
337            for (Iterator iter = fields.iterator(); iter.hasNext();i++) {
338                iter.next();
339                ps.setObject(i,null);
340            }
341            
342            return ps;
343        }
344        
345        /**
346         * Create a SELECT SQL request to fetch the field values of an object.
347         * Each field is mapped onto a SQL attribute.
348         * The table contains one more attribute which is the name of the wrappee,
349         * and which is also the primary key of the table.
350         * 
351         * @param connection  the connection towards the database
352         * @param cl          the class
353         * @return            the prepared statement
354         */
355        static private PreparedStatement createSelectStatement(
356            Connection connection, ClassItem cl ) throws SQLException {
357                
358            /**
359             * The table name is the class name where dots delimiting package names
360             * are replaced by underscores.
361             */
362            String tableName = cl.getName();
363            tableName = tableName.replace('.','_');
364            String request =
365                "SELECT * FROM "+tableName+" WHERE "+
366                objectAttributeName+"=?;";
367    
368            return connection.prepareStatement(request);
369        }
370    
371        /**
372         * Create an UPDATE SQL request to store the field values of an object.
373         * Each field is mapped onto a SQL attribute.
374         * The table contains one more attribute which is the name of the wrappee,
375         * and which is also the primary key of the table.
376         * 
377         * @param connection  the connection towards the database
378         * @param cl          the class
379         * @return            the prepared statement
380         */
381        static private PreparedStatement createUpdateStatement(
382            Connection connection, ClassItem cl ) throws SQLException {
383                
384            /**
385             * The table name is the class name where dots delimiting package names
386             * are replaced by underscores.
387             */
388            String tableName = cl.getName();
389            tableName = tableName.replace('.','_');
390            String request = "UPDATE "+tableName+" SET ";
391            
392            Collection fields = cl.getAllFields();
393            boolean first = true;
394            for (Iterator iter = fields.iterator(); iter.hasNext();) {
395                FieldItem field = (FieldItem) iter.next();
396                String name = field.getName();
397                
398                if (first) first = false;
399                else request += ",";
400                request += name + "=?";
401            }
402            
403            request += " WHERE "+objectAttributeName+"=?;";
404            
405            return connection.prepareStatement(request);
406        }
407    
408        /**
409         * The reference towards the RTTI class repository.
410         */
411        private ClassRepository classes = ClassRepository.get();
412    
413        /**
414         * The reference towards the JAC name repository.
415         * Needed by applyPersistence().
416         */
417        private Repository names = NameRepository.get();
418    }