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 }