001    /*
002      Copyright (C) 2001-2004 Laurent Martelli <laurent@aopsys.com>
003                              Renaud Pawlak <renaud@aopsys.com>
004    
005      This program is free software; you can redistribute it and/or modify
006      it under the terms of the GNU Lesser General Public License as
007      published by the Free Software Foundation; either version 2 of the
008      License, or (at your option) any later version.
009    
010      This program is distributed in the hope that it will be useful,
011      but WITHOUT ANY WARRANTY; without even the implied warranty of
012      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013      GNU Lesser General Public License for more details.
014    
015      You should have received a copy of the GNU Lesser General Public License
016      along with this program; if not, write to the Free Software
017      Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */
018    
019    package org.objectweb.jac.aspects.persistence;
020    
021    import gnu.regexp.RE;
022    import gnu.regexp.REException;
023    import java.util.Arrays;
024    import java.util.Collection;
025    import java.util.Date;
026    import java.util.HashMap;
027    import java.util.Hashtable;
028    import java.util.Iterator;
029    import java.util.Map;
030    import java.util.Vector;
031    import org.apache.log4j.Logger;
032    import org.objectweb.jac.core.AspectComponent;
033    import org.objectweb.jac.core.BaseProgramListener;
034    import org.objectweb.jac.core.Collaboration;
035    import org.objectweb.jac.core.MethodPointcut;
036    import org.objectweb.jac.core.NameRepository;
037    import org.objectweb.jac.core.Naming;
038    import org.objectweb.jac.core.ObjectRepository;
039    import org.objectweb.jac.core.SerializedJacObject;
040    import org.objectweb.jac.core.Wrappee;
041    import org.objectweb.jac.core.Wrapper;
042    import org.objectweb.jac.core.Wrapping;
043    import org.objectweb.jac.core.rtti.ClassItem;
044    import org.objectweb.jac.core.rtti.CollectionItem;
045    import org.objectweb.jac.core.rtti.FieldItem;
046    import org.objectweb.jac.util.ExtArrays;
047    
048    /**
049     * This AC defines a generic and configurable persistence aspect.<p>
050     */
051    
052    public class PersistenceAC extends AspectComponent implements PersistenceConf {
053    
054        static Logger logger = Logger.getLogger("persistence");
055        static Logger loggerCache = Logger.getLogger("persistence.cache");
056        static Logger loggerNaming = Logger.getLogger("persistence.naming");
057    
058        /** The global identifier for a persistence root class in the
059            RTTI. */
060        public static final String ROOT = "root";
061    
062        /** The global identifier for a persistent class in the RTTI. */
063        public static final String PERSISTENT = "persistent";
064    
065        public static final String VALUE_CONVERTER = "valueConverter";
066        public static final String PRELOAD_FIELD = "preloadField";
067    
068        public static final String DISABLE_PRELOAD = "PersistenceAC.DISABLE_PRELOAD";
069        public static final String NO_CACHE = "PersistenceAC.NO_CACHE";
070        public static final String RESTORE = "PersistenceAC.RESTORE";
071    
072        // Object -> OID
073        Hashtable oids = new Hashtable();
074        // OID -> Object
075        Hashtable objects = new Hashtable();
076    
077        private boolean connected = false;
078    
079        /**
080         * The default storage. */
081        protected StorageSpec defaultStorage = null;
082        /** List of StorageSpec */
083        protected Vector storages = new Vector();
084        class StorageSpec {
085            StorageSpec(String id,
086                        ClassItem storageClass, String[] parameters) 
087            {
088                this.id = id;
089                this.storageClass = storageClass;
090                this.parameters = parameters;
091            }
092            public void addClasses(String classExpr) throws REException {
093                classExprs = (RE[])
094                    ExtArrays.add(
095                        MethodPointcut.buildRegexp(classExpr),
096                        classExprs);
097            }
098            private RE[] classExprs = new RE[0];
099            private Storage storage;
100            private String[] parameters;
101            private ClassItem storageClass;
102            private String id;
103            synchronized Storage getStorage() {
104                if (storage == null) {
105                    if (storageClass == null) {
106                        throw new RuntimeException("Persistence: storage is not configured");
107                    }
108                    logger.debug(
109                        getClass().getName() + ".connectStorage("
110                        + storageClass + ","
111                        + (parameters != null
112                           ? Arrays.asList(parameters).toString()
113                           : "null")
114                        + ")");
115                    try {
116                        storage = 
117                            (Storage)storageClass.newInstance(
118                                ExtArrays.add(0,PersistenceAC.this,parameters,Object.class));
119                    } catch (Exception e) {
120                        logger.error("Failed to connect to storage ", e);
121                    }
122                }
123                return storage;
124            }
125            /**
126             * Tells wether instances of class should be stored on this storage.
127             */
128            boolean match(ClassItem cli) {
129                if (classExprs==null)
130                    return false;
131                String className = cli.getName();
132                for (int i=0; i<classExprs.length; i++) {
133                    if (classExprs[i].isMatch(className))
134                        return true;
135                }
136                return false;
137            }
138            String getId() {
139                return id;
140            }
141        }
142    
143        /**
144         * Gets the storage with a given id, or null.
145         * @param id id of the storage to get. If null, returns the defaultStorage.
146         */
147        protected Storage getStorage(String id) {
148            if (id==null) {
149                return defaultStorage.getStorage();
150            } else {
151                Iterator it = storages.iterator();
152                while (it.hasNext()) {
153                    StorageSpec storageSpec = (StorageSpec)it.next();
154                    if (id.equals(storageSpec.getId()))
155                        return storageSpec.getStorage();
156                }
157                logger.error("No such storage: "+id);
158                return null;
159            }
160        }
161    
162        protected Storage[] getStorages() {
163            Storage[] result = 
164                new Storage[storages.size()+ (defaultStorage!=null ? 1 : 0)];
165            Iterator it = storages.iterator();
166            int i=0;
167            while (it.hasNext()) {
168                StorageSpec storageSpec = (StorageSpec)it.next();
169                result[i] = storageSpec.getStorage();
170                i++;
171            }
172            if (defaultStorage!=null)
173                result[i] = defaultStorage.getStorage();
174            return result;
175        }
176    
177        /**
178         * The loaded objects from the storage. */
179        //   protected Hashtable objects = new Hashtable();
180    
181        /**
182         * Close every storage
183         */
184        public void onExit() {
185            Storage[] storages = getStorages();
186            for (int i=0; i<storages.length; i++){
187                storages[i].close();
188            }
189        }
190    
191        //private HashSet statics = new HashSet();
192    
193        // ---- PersistenceConf interface
194    
195        public void setValueConverter(ClassItem cl, ClassItem converterClass) {
196            StringConverter converter;
197            try {
198                converter = (StringConverter)converterClass.newInstance();
199            } catch (ClassCastException e) {
200                error("Converter class "+converterClass.getName()+
201                      " does not implement StringConverter");
202                return;
203            } catch (Exception e) {
204                error("Failed to instantiate value converter "+converterClass.getName());
205                return;
206            }
207            cl.setAttribute(VALUE_CONVERTER, converter);
208        }
209    
210        /**
211         * This method is a callback for the timer that defines the max
212         * idle time.
213         *
214         * <p>For all the collections that have a max idle time, it checks
215         * that this time is not reached. If it is reached, it unloads the
216         * collection.
217         *
218         * @see #defineMaxIdleCheckPeriod(long)
219         * @see #maxIdle(CollectionItem,long) */
220    
221        public void checkUnload() {
222    
223            Iterator it = collectionIdles.entrySet().iterator();
224            while (it.hasNext()) {
225                Map.Entry entry = (Map.Entry) it.next();
226    
227                CollectionItem coll = (CollectionItem) entry.getKey();
228                long idle = ((Long) entry.getValue()).longValue();
229    
230                loggerCache.debug("checking collection " + coll + " (idle=" + idle + ")");
231                ClassItem cl = coll.getClassItem();
232                Object[] objects = ObjectRepository.getMemoryObjects(cl);
233                for (int i = 0; i < objects.length; i++) {
234                    Wrappee wrappee = (Wrappee) coll.get(objects[i]);
235                    loggerCache.debug("checking wrappee "
236                              + NameRepository.get().getName(objects[i])
237                              + "." + wrappee);
238                    Date useDate =
239                        (Date) Wrapping.invokeRoleMethod(
240                            wrappee,
241                            CollectionWrapper.class,
242                            "getUseDate",
243                            ExtArrays.emptyObjectArray);
244                    Date now = new Date();
245                    long lastUsed = now.getTime() - useDate.getTime();
246                    loggerCache.debug("last used=" + lastUsed + " ms ago");
247                    if (lastUsed > idle) {
248                        loggerCache.debug("unloading collection "
249                                  + NameRepository.get().getName(objects[i])
250                                  + "." + coll);
251                        Wrapping.invokeRoleMethod(
252                            wrappee,
253                            CollectionWrapper.class,
254                            "unload",
255                            ExtArrays.emptyObjectArray);
256                    }
257                }
258            }
259        }
260    
261        long checkPeriod = -1;
262    
263        public void defineMaxIdleCheckPeriod(long period) {
264            checkPeriod = period;
265        }
266    
267        HashMap collectionIdles = new HashMap();
268    
269        public void maxIdle(CollectionItem collection, long maxIdle) {
270            collectionIdles.put(collection, new Long(maxIdle));
271        }
272    
273        public void whenConfigured() {
274            if (checkPeriod == -1) {
275                checkPeriod = 200000;
276            }
277            defineTimer(
278                checkPeriod,
279                cr.getClass(PersistenceAC.class).getMethod(
280                    "checkUnload"),
281                new Object[] {
282                });
283        }
284    
285        public void configureStorage(ClassItem storageClass,
286                                     String[] storageParameters) 
287        {
288            try {
289                this.defaultStorage = 
290                    new StorageSpec(null,storageClass,storageParameters);
291                this.defaultStorage.addClasses("ALL");
292            } catch (REException e) {
293                error(e.toString());
294            }
295        }
296    
297        public void configureStorage(String id,
298                                     ClassItem storageClass, 
299                                     String[] storageParameters) 
300        {
301            storages.add(
302                new StorageSpec(id,storageClass,storageParameters));
303        }
304    
305        public void setStorage(String classExpr, String storageId) {
306            try {
307                getStorageSpec(storageId).addClasses(classExpr);
308            } catch (REException e) {
309                error(e.toString());
310            }
311        }
312    
313        /** MethodPointcuts for static objects */
314        Vector staticPointcuts = new Vector();
315    
316        public void registerStatics(String classExpr, String nameExpr) {
317            /* Wrap constructors with PersistenceWrapper.handleStatic() */
318            logger.debug("registerStatics " + classExpr + " " + nameExpr);
319            staticPointcuts.add(
320                pointcut(
321                    nameExpr,
322                    classExpr,
323                    "CONSTRUCTORS",
324                    PersistenceWrapper.class.getName(),
325                    null,
326                    SHARED));
327        }
328    
329        /** Pointcuts for persistent objects */
330        Vector persistentPointcuts = new Vector();
331    
332        public void makePersistent(String classExpr, String nameExpr) {
333            // Wrap modifiers, collection accessors, reference accessors
334            // (but not constructors) with PersistenceWrapper.applyPersistence()
335            persistentPointcuts.add(
336                pointcut(
337                    nameExpr,
338                    classExpr,
339                    "MODIFIERS({!transient}) || COLACCESSORS({!transient}) || REFACCESSORS({!transient}) && !CONSTRUCTORS",
340                    PersistenceWrapper.class.getName(),
341                    null,
342                    NOT_SHARED));
343        }
344    
345        /**
346         * Tells wether a wrappee is persistent or not.
347         * @param wrappee the wrappee
348         * @see #makePersistent(String,String)
349         */
350        boolean isPersistent(Wrappee wrappee) {
351            Iterator it = persistentPointcuts.iterator();
352            ClassItem cli = cr.getClass(wrappee);
353            while (it.hasNext()) {
354                MethodPointcut pointcut = (MethodPointcut) it.next();
355                if (pointcut.isClassMatching(wrappee, cli))
356                    return true;
357            }
358            return false;
359        }
360    
361        /**
362         * Tells wether a wrappee is static or not.
363         * @param wrappee the wrapee
364         * @param objName name of the wrappee
365         * @see #makePersistent(String,String)
366         */
367        boolean isStatic(Wrappee wrappee, String objName) {
368            ClassItem cli = cr.getClass(wrappee);
369            // check the statics
370            Iterator it = staticPointcuts.iterator();
371            while (it.hasNext()) {
372                MethodPointcut pointcut = (MethodPointcut) it.next();
373                if (pointcut.isClassMatching(wrappee, cli)
374                    && pointcut.isNameMatching(wrappee, objName))
375                    return true;
376            }
377            return false;
378        }
379    
380        // ---- end of PersistenceConf
381    
382        /**
383         * Returns the storage for a given class
384         *
385         * @param cli a class
386         * @return the storage of the class
387         */
388        public Storage getStorage(ClassItem cli) {
389            Iterator it = storages.iterator();
390            while (it.hasNext()) {
391                StorageSpec storageSpec = (StorageSpec)it.next();
392                if (storageSpec.match(cli)) {
393                    return storageSpec.getStorage();
394                }
395            }
396            if (defaultStorage!=null)
397                return defaultStorage.getStorage();
398            throw new RuntimeException(
399                "Cannot find storage for class "+cli.getName());
400        }
401    
402        public StorageSpec getStorageSpec(String storageId) {
403            Iterator it = storages.iterator();
404            while (it.hasNext()) {
405                StorageSpec storageSpec = (StorageSpec)it.next();
406                if (storageSpec.getId().equals(storageId)) {
407                    return storageSpec;
408                }
409            }
410            throw new RuntimeException();
411        }
412    
413        /**
414         * Returns the storage for a given object
415         *
416         * @param cli a class
417         * @return the storage of the class
418         */
419        public Storage getStorage(Object obj) {
420            return getStorage(cr.getClass(obj));
421        }
422    
423        /**
424         * The persistence aspect checks whether an object was in the
425         * storage when a <code>NameRepository.getObject</code> call
426         * failed.
427         * @see BaseProgramListener#whenObjectMiss(String)
428         */
429        public void whenObjectMiss(String name) {
430            loggerNaming.debug("whenObjectMiss " + name);
431            // Loop through all the storages
432            Storage[] storages = getStorages();
433            for (int i=0; i<storages.length; i++){
434                try {
435                    OID oid = storages[i].getOIDFromName(name);
436                    if (oid != null) {
437                        loggerNaming.debug("found name " + name + " -> " + oid);
438                        attrdef(BaseProgramListener.FOUND_OBJECT, getObject(oid, null));
439                        return;
440                    }
441                } catch (Exception e) {
442                    logger.error("whenObjectMiss "+name,e);
443                }
444            }
445        }
446    
447        /**
448         * Delegates naming to the storage
449         */
450        public String whenNameObject(Object object, String name) {
451            loggerNaming.debug("whenNameObject " + object + " <- " + name);
452            if (!(object instanceof Wrappee))
453                return name;
454            Wrappee wrappee = (Wrappee) object;
455            if (isPersistent(wrappee) && !isStatic(wrappee, name)) {
456                try {
457                    name = getStorage(object).newName(object.getClass().getName());
458                } catch (Exception e) {
459                    logger.error("Failed to name object "+object,e);
460                }
461                loggerNaming.debug("   -> " + name);
462            }
463            return name;
464        }
465    
466        public void getNameCounters(Map counters) {
467            try {
468                Storage[] storages = getStorages();
469                for(int i=0; i<storages.length; i++) {
470                    counters.putAll(storages[i].getNameCounters());                
471                }
472            } catch (Exception e) {
473                logger.error("getNameCounters failed",e);
474            }
475        }
476    
477        public synchronized void updateNameCounters(Map counters) {
478            try {
479                Storage[] storages = getStorages();
480                for(int i=0; i<storages.length; i++) {
481                    storages[i].updateNameCounters(counters);
482                }
483            } catch (Exception e) {
484                logger.error("updateNameCounters failed",e);
485            }
486        }
487    
488        /**
489         * Add an object in the list of persistent objects.
490         *
491         * @param oid the OID of the object
492         * @param object the object
493         */
494        protected void registerObject(OID oid, Object object) {
495            logger.debug("registerObject(" + oid + "," + object.getClass() + ")");
496            Object currentObject = objects.get(oid);
497            if (currentObject!=null) {
498                if (currentObject!=object) {
499                    logger.error("registerObject "+oid+","+object,new Exception());
500                    throw new Error(
501                        "PersistenceAC.registerObject("+oid+","+object+"): an object "+
502                        currentObject+" is already registered with this oid");
503                } else {
504                    logger.warn("PersistenceAC.registerObject("+oid+","+object+
505                                "): already registered");
506                }
507            }
508            objects.put(oid, object);
509            oids.put(object, oid);
510            logger.debug("object " + oid + " added");
511        }
512    
513        /**
514         * Returns a reference to an object with a given OID.
515         * 
516         * <p>Loads the object from a storage if necessary, or returns a
517         * cached object.</p>
518         *
519         * @param oid OID of the object 
520         * @param newObject use this object instead of instanciating a new one
521         */
522        synchronized Object getObject(OID oid, Object newObject) {
523            Object result = objects.get(oid);
524            try {
525                if (result != null) {
526                    logger.debug("Object " + oid + " found in cache -> " + result);
527                    return result;
528                } else {
529                    logger.debug(this + ".Object " + oid
530                              + " NOT found in cache; Loading from storage\n");
531                    Storage storage = oid.getStorage();
532                    String lClassID = storage.getClassID(oid);
533                    if (lClassID == null)
534                        logger.error("getClassID(" + oid + ") -> NULL");
535                    ClassItem lClass = cr.getClass(lClassID);
536                    logger.debug("Class = " + lClass.getName());
537                    if (newObject == null) {
538                        Naming.setName(storage.getNameFromOID(oid));
539                        Collaboration collab = Collaboration.get();
540                        collab.addAttribute(RESTORE, Boolean.TRUE);
541                        try {
542                            newObject = lClass.newInstance();
543                        } finally {
544                            collab.removeAttribute(RESTORE);
545                        }
546                    }
547                    Wrappee wrappee = (Wrappee) newObject;
548                                    //PersistenceWrapper wrapper = wrap(wrappee,oid);
549                    registerObject(oid, wrappee);
550                                    // load the new object's fields
551                    Wrapping
552                        .invokeRoleMethod(
553                            wrappee,
554                            PersistenceWrapper.class,
555                            "loadAllFields",
556                            new Object[] {oid});
557                                    // wrap its collections
558                    Wrapping.invokeRoleMethod(
559                        wrappee,
560                        PersistenceWrapper.class,
561                        "wrapCollections",
562                        new Object[] { oid, Boolean.FALSE });
563                    logger.debug("New object " + oid + " : " + newObject);
564                    logger.debug("Object loaded");
565                    result = newObject;
566                }
567            } catch (Exception e) {
568                logger.error("getObject "+oid,e);
569            }
570            return result;
571        }
572    
573        public OID getOID(Wrappee wrappee) {
574            return (OID) oids.get(wrappee);
575        }
576    
577        /**
578         * This method allows the deserialization of the OID of a
579         * persistent object.<p>
580         *
581         * @param orgObject the JAC object structure that is beeing
582         * deserialized
583         * @see #whenSerialized(SerializedJacObject) */
584    
585        public void whenDeserialized(SerializedJacObject orgObject) {
586            /*      
587                    OID oid = (OID) orgObject.getACInfos( "persistence" );
588                    if ( oid != null ) {
589                    Wrappee finalObject = (Wrappee)attr("finalObject");
590                    PersistenceWrapper pw = wrap( finalObject, oid, false );
591                    //pw.setOid( oid );
592                    }
593            */
594        }
595    
596        /**
597         * This method add the OID info to the serialized JAC object when a
598         * serialization is requested by another aspect.<p>
599         *
600         * This adding allows, for instance, the OID to be transmitted to
601         * remote containers.<p>
602         *
603         * @param finalObject the object that is being serialized */
604    
605        public void whenSerialized(SerializedJacObject finalObject) {
606            /*  
607                Wrappee orgObject = (Wrappee)attr("orgObject");
608                if ( orgObject.isExtendedBy( PersistenceWrapper.class ) ) {
609                finalObject.setACInfos(
610                "persistence", orgObject.invokeRoleMethod( "getOid", ExtArrays.emptyObjectArray ) );
611                }
612            */
613        }
614    
615        /**
616         * Load objects from the storage when required.
617         */
618        public void whenGetObjects(Collection objects, ClassItem cl) {
619            logger.debug("PersistenceAC.whenGetObjects " + cl);
620            if (cl == null)
621                return;
622            try {
623                logger.debug("PersistenceAC.whenGetObjects " + cl);
624                Collection oids = getStorage(cl).getObjects(cl);
625                Iterator i = oids.iterator();
626                while (i.hasNext()) {
627                    OID oid = (OID) i.next();
628                    Object object = getObject(oid, null);
629                    if (!objects.contains(object))
630                        objects.add(object);
631                }
632            } catch (Exception e) {
633                e.printStackTrace();
634            }
635        }
636    
637        public boolean beforeRunningWrapper(
638            Wrapper wrapper,
639            String wrappingMethod) {
640            if (attr("Persistence.disabled") != null) {
641                return false;
642            } else {
643                return true;
644            }
645        }
646    
647        public void whenDeleted(Wrappee object) {
648            try {
649                OID oid = getOID(object);
650                Storage storage = oid.getStorage();
651                if (storage != null) {
652                    if (oid != null)
653                        storage.deleteObject(oid);
654                    whenFree(object);
655                }
656            } catch (Exception e) {
657                logger.error("whenDeleted "+object+" failed",e);
658            }
659        }
660    
661        public void whenFree(Wrappee object) {
662            try {
663                OID oid = getOID(object);
664                Storage storage = oid.getStorage();
665                if (storage != null) {
666                    if (oid != null)
667                        oids.remove(oid);
668                    if (objects.contains(object))
669                        objects.remove(object);
670                }
671            } catch (Exception e) {
672                logger.error("whenFree "+object+" failed",e);
673            }
674        }
675    
676        public void preloadField(FieldItem field) {
677            field.setAttribute(PRELOAD_FIELD, Boolean.TRUE);
678        }
679    
680        public void preloadAllFields(ClassItem cl) {
681            FieldItem[] fields = cl.getFields();
682            if (fields != null) {
683                for (int i = 0; i < fields.length; i++) {
684                    fields[i].setAttribute(PRELOAD_FIELD, Boolean.TRUE);
685                }
686            }
687        }
688    
689        public static boolean isFieldPreloaded(FieldItem field) {
690            Boolean value = (Boolean) field.getAttribute(PRELOAD_FIELD);
691            return value != null && value.booleanValue();
692        }
693    
694        public void disableCache(CollectionItem collection) {
695            collection.setAttribute(NO_CACHE, "true");
696        }
697    
698        public String[] getDefaultConfigs() {
699            return new String[] {
700                "org/objectweb/jac/aspects/persistence/persistence.acc",
701                "org/objectweb/jac/aspects/user/persistence.acc" };
702        }
703    
704        public static class NoSuchWrapperException extends RuntimeException {
705        }
706    
707        /**
708         * Converts a String into a LongOID. 
709         * @param str the string representing the OID. If it's like
710         * "<number>@<string>", the "<string>" must be a storage
711         * id. Otherwise, the defaultStorage is used
712         * @param defaultStorage storage to use to build the OID if the
713         * string does not contain an OID part.
714         */
715        public LongOID parseLongOID(String str, Storage defaultStorage) {
716            int index = str.indexOf('@');
717            if (index==-1) {
718                return 
719                    new LongOID(defaultStorage,Long.parseLong(str));
720            } else {
721                return 
722                    new LongOID(
723                        getStorage(str.substring(index+1)),
724                        Long.parseLong(str.substring(0,index)));
725            }
726        }
727    }