001    package org.apache.fulcrum.intake;
002    
003    /*
004     * Licensed to the Apache Software Foundation (ASF) under one
005     * or more contributor license agreements.  See the NOTICE file
006     * distributed with this work for additional information
007     * regarding copyright ownership.  The ASF licenses this file
008     * to you under the Apache License, Version 2.0 (the
009     * "License"); you may not use this file except in compliance
010     * with the License.  You may obtain a copy of the License at
011     *
012     *   http://www.apache.org/licenses/LICENSE-2.0
013     *
014     * Unless required by applicable law or agreed to in writing,
015     * software distributed under the License is distributed on an
016     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017     * KIND, either express or implied.  See the License for the
018     * specific language governing permissions and limitations
019     * under the License.
020     */
021    
022    import java.beans.IntrospectionException;
023    import java.beans.PropertyDescriptor;
024    import java.io.File;
025    import java.io.FileInputStream;
026    import java.io.FileOutputStream;
027    import java.io.InputStream;
028    import java.io.ObjectInputStream;
029    import java.io.ObjectOutputStream;
030    import java.io.OutputStream;
031    import java.lang.reflect.Method;
032    import java.util.ArrayList;
033    import java.util.HashMap;
034    import java.util.HashSet;
035    import java.util.Iterator;
036    import java.util.List;
037    import java.util.Map;
038    import java.util.Set;
039    
040    import org.apache.avalon.framework.activity.Initializable;
041    import org.apache.avalon.framework.configuration.Configurable;
042    import org.apache.avalon.framework.configuration.Configuration;
043    import org.apache.avalon.framework.configuration.ConfigurationException;
044    import org.apache.avalon.framework.context.Context;
045    import org.apache.avalon.framework.context.ContextException;
046    import org.apache.avalon.framework.context.Contextualizable;
047    import org.apache.avalon.framework.logger.AbstractLogEnabled;
048    import org.apache.avalon.framework.service.ServiceException;
049    import org.apache.avalon.framework.service.ServiceManager;
050    import org.apache.avalon.framework.service.Serviceable;
051    import org.apache.commons.pool.KeyedObjectPool;
052    import org.apache.commons.pool.KeyedPoolableObjectFactory;
053    import org.apache.commons.pool.impl.StackKeyedObjectPool;
054    import org.apache.fulcrum.intake.model.Group;
055    import org.apache.fulcrum.intake.transform.XmlToAppData;
056    import org.apache.fulcrum.intake.xmlmodel.AppData;
057    import org.apache.fulcrum.intake.xmlmodel.XmlGroup;
058    
059    /**
060     * This service provides access to input processing objects based on an XML
061     * specification.
062     *
063     * @author <a href="mailto:jmcnally@collab.net">John McNally</a>
064     * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
065     * @author <a href="mailto:quintonm@bellsouth.net">Quinton McCombs</a>
066     * @version $Id: IntakeServiceImpl.java 832048 2009-11-02 18:55:08Z tv $
067     *
068     * @avalon.component name="intake"
069     * @avalon.service type="org.apache.fulcrum.intake.IntakeService"
070     */
071    public class IntakeServiceImpl extends AbstractLogEnabled implements
072            IntakeService, Configurable, Initializable, Contextualizable,
073            Serviceable
074    {
075        /** Map of groupNames -> appData elements */
076        private Map groupNames;
077    
078        /** The cache of group names. */
079        private Map groupNameMap;
080    
081        /** The cache of group keys. */
082        private Map groupKeyMap;
083    
084        /** The cache of property getters. */
085        private Map getterMap;
086    
087        /** The cache of property setters. */
088        private Map setterMap;
089    
090        /** AppData -> keyed Pools Map */
091        private Map keyedPools;
092    
093        /** The Avalon Container root directory */
094        private String applicationRoot;
095    
096        /** List of configured xml specification files */
097        private List xmlPathes = null;
098        
099        /** Configured location of the serialization file */
100        private String serialDataPath = null; 
101        
102        /**
103         * Registers a given group name in the system
104         *
105         * @param groupName
106         *            The name to register the group under
107         * @param group
108         *            The XML Group to register in
109         * @param appData
110         *            The app Data object where the group can be found
111         * @param checkKey
112         *            Whether to check if the key also exists.
113         *
114         * @return true if successful, false if not
115         */
116        private boolean registerGroup(String groupName, XmlGroup group,
117                AppData appData, boolean checkKey)
118        {
119            if (groupNames.keySet().contains(groupName))
120            {
121                // This name already exists.
122                return false;
123            }
124    
125            boolean keyExists = groupNameMap.keySet().contains(group.getKey());
126    
127            if (checkKey && keyExists)
128            {
129                // The key for this package is already registered for another group
130                return false;
131            }
132    
133            groupNames.put(groupName, appData);
134    
135            groupKeyMap.put(groupName, group.getKey());
136    
137            if (!keyExists)
138            {
139                // This key does not exist. Add it to the hash.
140                groupNameMap.put(group.getKey(), groupName);
141            }
142    
143            List classNames = group.getMapToObjects();
144            for (Iterator iter2 = classNames.iterator(); iter2.hasNext();)
145            {
146                String className = (String) iter2.next();
147                if (!getterMap.containsKey(className))
148                {
149                    getterMap.put(className, new HashMap());
150                    setterMap.put(className, new HashMap());
151                }
152            }
153            return true;
154        }
155    
156        /**
157         * Tries to load a serialized Intake Group file. This can reduce the startup
158         * time of Turbine.
159         *
160         * @param serialDataPath
161         *            The path of the File to load.
162         *
163         * @return A map with appData objects loaded from the file or null if the
164         *         map could not be loaded.
165         */
166        private Map loadSerialized(String serialDataPath, long timeStamp)
167        {
168            getLogger().debug(
169                    "Entered loadSerialized(" + serialDataPath + ", " + timeStamp
170                            + ")");
171    
172            if (serialDataPath == null)
173            {
174                return null;
175            }
176    
177            File serialDataFile = new File(serialDataPath);
178    
179            if (!serialDataFile.exists())
180            {
181                getLogger().info("No serialized file found, parsing XML");
182                return null;
183            }
184    
185            if (serialDataFile.lastModified() <= timeStamp)
186            {
187                getLogger().info("serialized file too old, parsing XML");
188                return null;
189            }
190    
191            InputStream in = null;
192            Map serialData = null;
193    
194            try
195            {
196                in = new FileInputStream(serialDataFile);
197                ObjectInputStream p = new ObjectInputStream(in);
198                Object o = p.readObject();
199    
200                if (o instanceof Map)
201                {
202                    serialData = (Map) o;
203                }
204                else
205                {
206                    // Maybe an old file from intake. Ignore it and try to delete
207                    getLogger().info(
208                            "serialized object is not an intake map, ignoring");
209                    in.close();
210                    in = null;
211                    serialDataFile.delete(); // Try to delete the file lying
212                                                // around
213                }
214            }
215            catch (Exception e)
216            {
217                getLogger().error("Serialized File could not be read.", e);
218    
219                // We got a corrupt file for some reason.
220                // Null out serialData to be sure
221                serialData = null;
222            }
223            finally
224            {
225                // Could be null if we opened a file, didn't find it to be a
226                // Map object and then nuked it away.
227                try
228                {
229                    if (in != null)
230                    {
231                        in.close();
232                    }
233                }
234                catch (Exception e)
235                {
236                    getLogger().error("Exception while closing file", e);
237                }
238            }
239    
240            getLogger().info("Loaded serialized map object, ignoring XML");
241            return serialData;
242        }
243    
244        /**
245         * Writes a parsed XML map with all the appData groups into a file. This
246         * will speed up loading time when you restart the Intake Service because it
247         * will only unserialize this file instead of reloading all of the XML files
248         *
249         * @param serialDataPath
250         *            The path of the file to write to
251         * @param appDataElements
252         *            A Map containing all of the XML parsed appdata elements
253         */
254        private void saveSerialized(String serialDataPath, Map appDataElements)
255        {
256    
257            getLogger().debug(
258                    "Entered saveSerialized(" + serialDataPath
259                            + ", appDataElements)");
260    
261            if (serialDataPath == null)
262            {
263                return;
264            }
265    
266            File serialData = new File(serialDataPath);
267    
268            try
269            {
270                serialData.createNewFile();
271                serialData.delete();
272            }
273            catch (Exception e)
274            {
275                getLogger().info(
276                        "Could not create serialized file " + serialDataPath
277                                + ", not serializing the XML data");
278                return;
279            }
280    
281            OutputStream out = null;
282            InputStream in = null;
283    
284            try
285            {
286                // write the appData file out
287                out = new FileOutputStream(serialDataPath);
288                ObjectOutputStream pout = new ObjectOutputStream(out);
289                pout.writeObject(appDataElements);
290                pout.flush();
291    
292                // read the file back in. for some reason on OSX 10.1
293                // this is necessary.
294                in = new FileInputStream(serialDataPath);
295                ObjectInputStream pin = new ObjectInputStream(in);
296                /* Map dummy = (Map) */ pin.readObject();
297    
298                getLogger().debug("Serializing successful");
299            }
300            catch (Exception e)
301            {
302                getLogger().info(
303                        "Could not write serialized file to " + serialDataPath
304                                + ", not serializing the XML data");
305            }
306            finally
307            {
308                try
309                {
310                    if (out != null)
311                    {
312                        out.close();
313                    }
314                    if (in != null)
315                    {
316                        in.close();
317                    }
318                }
319                catch (Exception e)
320                {
321                    getLogger().error("Exception while closing file", e);
322                }
323            }
324        }
325    
326        /**
327         * Gets an instance of a named group either from the pool or by calling the
328         * Factory Service if the pool is empty.
329         *
330         * @param groupName
331         *            the name of the group.
332         * @return a Group instance.
333         * @throws IntakeException
334         *             if recycling fails.
335         */
336        public Group getGroup(String groupName) throws IntakeException
337        {
338            Group group = null;
339    
340            AppData appData = (AppData) groupNames.get(groupName);
341    
342            if (groupName == null)
343            {
344                throw new IntakeException(
345                        "Intake IntakeServiceImpl.getGroup(groupName) is null");
346            }
347            if (appData == null)
348            {
349                throw new IntakeException(
350                        "Intake IntakeServiceImpl.getGroup(groupName): No XML definition for Group "
351                                + groupName + " found");
352            }
353            try
354            {
355                group = (Group) ((KeyedObjectPool) keyedPools.get(appData))
356                        .borrowObject(groupName);
357            }
358            catch (Exception e)
359            {
360                throw new IntakeException("Could not get group " + groupName, e);
361            }
362            return group;
363        }
364    
365        /**
366         * Puts a Group back to the pool.
367         *
368         * @param instance
369         *            the object instance to recycle.
370         *
371         * @throws IntakeException
372         *             The passed group name does not exist.
373         */
374        public void releaseGroup(Group instance) throws IntakeException
375        {
376            if (instance != null)
377            {
378                String groupName = instance.getIntakeGroupName();
379                AppData appData = (AppData) groupNames.get(groupName);
380    
381                if (appData == null)
382                {
383                    throw new IntakeException(
384                            "Intake IntakeServiceImpl.releaseGroup(groupName): "
385                                    + "No XML definition for Group " + groupName
386                                    + " found");
387                }
388    
389                try
390                {
391                    ((KeyedObjectPool) keyedPools.get(appData)).returnObject(
392                            groupName, instance);
393                }
394                catch (Exception e)
395                {
396                    new IntakeException("Could not get group " + groupName, e);
397                }
398            }
399        }
400    
401        /**
402         * Gets the current size of the pool for a group.
403         *
404         * @param groupName
405         *            the name of the group.
406         *
407         * @throws IntakeException
408         *             The passed group name does not exist.
409         */
410        public int getSize(String groupName) throws IntakeException
411        {
412            AppData appData = (AppData) groupNames.get(groupName);
413            if (appData == null)
414            {
415                throw new IntakeException(
416                        "Intake IntakeServiceImpl.Size(groupName): No XML definition for Group "
417                                + groupName + " found");
418            }
419    
420            KeyedObjectPool kop = (KeyedObjectPool) keyedPools.get(groupName);
421    
422            return kop.getNumActive(groupName) + kop.getNumIdle(groupName);
423        }
424    
425        /**
426         * Names of all the defined groups.
427         *
428         * @return array of names.
429         */
430        public String[] getGroupNames()
431        {
432            return (String[]) groupNames.keySet().toArray(new String[0]);
433        }
434    
435        /**
436         * Gets the key (usually a short identifier) for a group.
437         *
438         * @param groupName
439         *            the name of the group.
440         * @return the the key.
441         */
442        public String getGroupKey(String groupName)
443        {
444            return (String) groupKeyMap.get(groupName);
445        }
446    
447        /**
448         * Gets the group name given its key.
449         *
450         * @param groupKey
451         *            the key.
452         * @return groupName the name of the group.
453         */
454        public String getGroupName(String groupKey)
455        {
456            return (String) groupNameMap.get(groupKey);
457        }
458    
459        /**
460         * Gets the Method that can be used to set a property.
461         *
462         * @param className
463         *            the name of the object.
464         * @param propName
465         *            the name of the property.
466         * @return the setter.
467         * @throws ClassNotFoundException
468         * @throws IntrospectionException
469         */
470        public Method getFieldSetter(String className, String propName)
471                throws ClassNotFoundException, IntrospectionException
472        {
473            Map settersForClassName = (Map) setterMap.get(className);
474    
475            if (settersForClassName == null)
476            {
477                throw new IntrospectionException("No setter Map for " + className
478                        + " available!");
479            }
480    
481            Method setter = (Method) settersForClassName.get(propName);
482    
483            if (setter == null)
484            {
485                PropertyDescriptor pd = new PropertyDescriptor(propName, Class
486                        .forName(className));
487                synchronized (setterMap)
488                {
489                    setter = pd.getWriteMethod();
490                    settersForClassName.put(propName, setter);
491                    if (setter == null)
492                    {
493                        getLogger().error(
494                                "Intake: setter for '" + propName + "' in class '"
495                                        + className + "' could not be found.");
496                    }
497                }
498                // we have already completed the reflection on the getter, so
499                // save it so we do not have to repeat
500                synchronized (getterMap)
501                {
502                    Map gettersForClassName = (Map) getterMap.get(className);
503    
504                    if (gettersForClassName != null)
505                    {
506                        Method getter = pd.getReadMethod();
507                        if (getter != null)
508                        {
509                            gettersForClassName.put(propName, getter);
510                        }
511                    }
512                }
513            }
514            return setter;
515        }
516    
517        /**
518         * Gets the Method that can be used to get a property value.
519         *
520         * @param className
521         *            the name of the object.
522         * @param propName
523         *            the name of the property.
524         * @return the getter.
525         * @throws ClassNotFoundException
526         * @throws IntrospectionException
527         */
528        public Method getFieldGetter(String className, String propName)
529                throws ClassNotFoundException, IntrospectionException
530        {
531            Map gettersForClassName = (Map) getterMap.get(className);
532    
533            if (gettersForClassName == null)
534            {
535                throw new IntrospectionException("No getter Map for " + className
536                        + " available!");
537            }
538    
539            Method getter = (Method) gettersForClassName.get(propName);
540    
541            if (getter == null)
542            {
543                PropertyDescriptor pd = null;
544                synchronized (getterMap)
545                {
546                    pd = new PropertyDescriptor(propName, Class.forName(className));
547                    getter = pd.getReadMethod();
548                    gettersForClassName.put(propName, getter);
549                    if (getter == null)
550                    {
551                        getLogger().error(
552                                "Intake: getter for '" + propName + "' in class '"
553                                        + className + "' could not be found.");
554                    }
555                }
556                // we have already completed the reflection on the setter, so
557                // save it so we do not have to repeat
558                synchronized (setterMap)
559                {
560                    Map settersForClassName = (Map) getterMap.get(className);
561    
562                    if (settersForClassName != null)
563                    {
564                        Method setter = pd.getWriteMethod();
565                        if (setter != null)
566                        {
567                            settersForClassName.put(propName, setter);
568                        }
569                    }
570                }
571            }
572            return getter;
573        }
574    
575        // ---------------- Avalon Lifecycle Methods ---------------------
576        /**
577         * Avalon component lifecycle method
578         */
579        public void configure(Configuration conf) throws ConfigurationException
580        {
581            final Configuration xmlPaths = conf.getChild(XML_PATHS, false);
582    
583            xmlPathes = new ArrayList();
584    
585            if (xmlPaths == null)
586            {
587                xmlPathes.add(XML_PATH_DEFAULT);
588            }
589            else
590            {
591                Configuration[] nameVal = xmlPaths.getChildren();
592                for (int i = 0; i < nameVal.length; i++)
593                {
594                    String val = nameVal[i].getValue();
595                    xmlPathes.add(val);
596                }
597            }
598    
599            serialDataPath = conf.getChild(SERIAL_XML, false).getValue(SERIAL_XML_DEFAULT);
600    
601            if (!serialDataPath.equalsIgnoreCase("none"))
602            {
603                serialDataPath = new File(applicationRoot, serialDataPath).getAbsolutePath();
604            }
605            else
606            {
607                serialDataPath = null;
608            }
609    
610            getLogger().debug("Path for serializing: " + serialDataPath);
611        }
612    
613        /**
614         * Avalon component lifecycle method Initializes the service by loading
615         * default class loaders and customized object factories.
616         *
617         * @throws Exception
618         *             if initialization fails.
619         */
620        public void initialize() throws Exception
621        {
622            Map appDataElements = null;
623    
624            groupNames = new HashMap();
625            groupKeyMap = new HashMap();
626            groupNameMap = new HashMap();
627            getterMap = new HashMap();
628            setterMap = new HashMap();
629            keyedPools = new HashMap();
630    
631            Set xmlFiles = new HashSet();
632    
633            long timeStamp = 0;
634    
635            for (Iterator it = xmlPathes.iterator(); it.hasNext();)
636            {
637                // Files are webapp.root relative
638                String xmlPath = (String) it.next();
639                File xmlFile = new File(applicationRoot, xmlPath).getAbsoluteFile();
640    
641                getLogger().debug("Path for XML File: " + xmlFile);
642    
643                if (!xmlFile.canRead())
644                {
645                    String READ_ERR = "Could not read input file with path "
646                            + xmlPath + ".  Looking for file " + xmlFile;
647    
648                    getLogger().error(READ_ERR);
649                    throw new Exception(READ_ERR);
650                }
651    
652                xmlFiles.add(xmlFile.toString());
653    
654                getLogger().debug("Added " + xmlPath + " as File to parse");
655    
656                // Get the timestamp of the youngest file to be compared with
657                // a serialized file. If it is younger than the serialized file,
658                // then we have to parse the XML anyway.
659                timeStamp = (xmlFile.lastModified() > timeStamp) ? xmlFile
660                        .lastModified() : timeStamp;
661            }
662    
663            Map serializedMap = loadSerialized(serialDataPath, timeStamp);
664    
665            if (serializedMap != null)
666            {
667                // Use the serialized data as XML groups. Don't parse.
668                appDataElements = serializedMap;
669                getLogger().debug("Using the serialized map");
670            }
671            else
672            {
673                // Parse all the given XML files
674                appDataElements = new HashMap();
675    
676                for (Iterator it = xmlFiles.iterator(); it.hasNext();)
677                {
678                    String xmlPath = (String) it.next();
679                    AppData appData = null;
680    
681                    getLogger().debug("Now parsing: " + xmlPath);
682    
683                    XmlToAppData xmlApp = new XmlToAppData();
684                    xmlApp.enableLogging(getLogger());
685                    appData = xmlApp.parseFile(xmlPath);
686    
687                    appDataElements.put(appData, xmlPath);
688                    getLogger().debug("Saving appData for " + xmlPath);
689                }
690    
691                saveSerialized(serialDataPath, appDataElements);
692            }
693    
694            for (Iterator it = appDataElements.keySet().iterator(); it.hasNext();)
695            {
696                AppData appData = (AppData) it.next();
697    
698                int maxPooledGroups = 0;
699                List glist = appData.getGroups();
700    
701                String groupPrefix = appData.getGroupPrefix();
702    
703                for (int i = glist.size() - 1; i >= 0; i--)
704                {
705                    XmlGroup g = (XmlGroup) glist.get(i);
706                    String groupName = g.getName();
707    
708                    boolean registerUnqualified = registerGroup(groupName, g,
709                            appData, true);
710    
711                    if (!registerUnqualified)
712                    {
713                        getLogger().info(
714                                "Ignored redefinition of Group " + groupName
715                                        + " or Key " + g.getKey() + " from "
716                                        + appDataElements.get(appData));
717                    }
718    
719                    if (groupPrefix != null)
720                    {
721                        StringBuffer qualifiedName = new StringBuffer();
722                        qualifiedName.append(groupPrefix).append(':').append(
723                                groupName);
724    
725                        // Add the fully qualified group name. Do _not_ check
726                        // for
727                        // the existence of the key if the unqualified
728                        // registration succeeded
729                        // (because then it was added by the registerGroup
730                        // above).
731                        if (!registerGroup(qualifiedName.toString(), g,
732                                appData, !registerUnqualified))
733                        {
734                            getLogger().error(
735                                "Could not register fully qualified name "
736                                        + qualifiedName
737                                        + ", maybe two XML files have the same prefix. Ignoring it.");
738                        }
739                    }
740    
741                    maxPooledGroups = Math.max(maxPooledGroups, Integer
742                            .parseInt(g.getPoolCapacity()));
743    
744                }
745    
746                KeyedPoolableObjectFactory factory = new Group.GroupFactory(
747                        appData);
748                keyedPools.put(appData, new StackKeyedObjectPool(factory,
749                        maxPooledGroups));
750            }
751    
752            if (getLogger().isInfoEnabled())
753            {
754                getLogger().info("Intake Service is initialized now.");
755            }
756        }
757    
758        /**
759         * @see org.apache.avalon.framework.context.Contextualizable
760         * @avalon.entry key="urn:avalon:home" type="java.io.File"
761         */
762        public void contextualize(Context context) throws ContextException
763        {
764            this.applicationRoot = context.get("urn:avalon:home").toString();
765        }
766    
767        /**
768         * Avalon component lifecycle method
769         *
770         * @avalon.dependency type="org.apache.fulcrum.localization.LocalizationService"
771         */
772        public void service(ServiceManager manager) throws ServiceException
773        {
774            IntakeServiceFacade.setIntakeService(this);
775        }
776    }