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 }