View Javadoc
1 /* ==================================================================== 2 * Bigyo Software License, version 1.1 3 * 4 * Copyright (c) 2004, Zsombor Gegesy. All rights reserved. 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions 7 * are met: 8 * 9 * 1. Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * 12 * 2. Redistributions in binary form must reproduce the above copyright 13 * notice, this list of conditions and the following disclaimer in 14 * the documentation and/or other materials provided with the 15 * distribution. 16 * 17 * 3. Neither the name of the Bigyo Group nor the name "Bigyo" nor 18 * the names of its contributors may be used to endorse or promote 19 * products derived from this software without specific prior 20 * written permission. 21 * 22 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33 * POSSIBILITY OF SUCH DAMAGE. 34 * 35 * ==================================================================== 36 */ 37 38 39 package net.sf.bigyo.container; 40 41 import java.io.BufferedInputStream; 42 import java.io.BufferedOutputStream; 43 import java.io.File; 44 import java.io.FileFilter; 45 import java.io.FileInputStream; 46 import java.io.FileNotFoundException; 47 import java.io.FileOutputStream; 48 import java.io.IOException; 49 import java.io.InputStreamReader; 50 import java.io.OutputStreamWriter; 51 import java.io.UnsupportedEncodingException; 52 import java.util.ArrayList; 53 import java.util.HashMap; 54 import java.util.Iterator; 55 import java.util.List; 56 import java.util.Map; 57 58 import net.sf.bigyo.api.ContainerException; 59 import net.sf.bigyo.api.ReconfigurationManager; 60 import net.sf.bigyo.container.api.Constants; 61 import net.sf.bigyo.container.api.Repository; 62 import net.sf.bigyo.container.config.ConfigurationStrategy; 63 import net.sf.bigyo.container.config.ReconfigurationNotSupportedException; 64 import net.sf.bigyo.container.config.SimpleConfigurationStrategy; 65 import net.sf.bigyo.model.ComponentConfig; 66 import net.sf.bigyo.model.ObjectDependency; 67 68 import org.apache.avalon.fortress.util.dag.CyclicDependencyException; 69 import org.apache.avalon.fortress.util.dag.DirectedAcyclicGraphVerifier; 70 import org.apache.avalon.fortress.util.dag.Vertex; 71 import org.apache.log4j.LogManager; 72 import org.apache.log4j.Logger; 73 74 import com.thoughtworks.xstream.converters.ConversionException; 75 import com.thoughtworks.xstream.io.StreamException; 76 77 /*** 78 * RepositoryImpl manages the component instances, (create, configure, 79 * reload...stop) 80 * 81 * Created on 2004.04.01. 82 * @author zsombor 83 * 84 */ 85 class RepositoryImpl implements Repository, ReconfigurationManager { 86 87 final static Logger LOG = LogManager.getLogger(Main.class); 88 89 boolean panicIfDuplicates = true; 90 boolean createBackups = true; 91 92 List vertices = new ArrayList(); 93 Main main; 94 95 Map vertMap = new HashMap(); 96 97 List compConfigs = new ArrayList(); 98 Map componentConfigMapping = new HashMap(); 99 List startupComponents = new ArrayList(); 100 Map fileToConfigMap = new HashMap(); 101 Map componentToPersistedSource = new HashMap(); 102 103 private ConfigurationStrategy configurationStrategy; 104 105 static String xmlFileEncoding = "ISO-8859-1"; 106 107 static { 108 xmlFileEncoding = System.getProperty("xml.file.encoding", xmlFileEncoding); 109 LOG.info("xml file encoding set to " + xmlFileEncoding); 110 } 111 112 /*** 113 * 114 */ 115 RepositoryImpl(Main m) { 116 this.main = m; 117 this.configurationStrategy = new SimpleConfigurationStrategy(); 118 ComponentConfig serviceLocator = new ComponentConfig(); 119 serviceLocator.setInstanceName(Constants.SERVICE_LOCATOR); 120 serviceLocator.setClassAlias(Constants.SERVICE_LOCATOR); 121 addConfig(null, serviceLocator); 122 123 ComponentConfig reconfManager = new ComponentConfig(); 124 reconfManager.setInstanceName(Constants.RECONFIGURATION_MANAGER); 125 reconfManager.setClassAlias(Constants.RECONFIGURATION_MANAGER); 126 addConfig(null, reconfManager); 127 } 128 129 /*** 130 * betolti az adott konyvtarban levo osszes .conf.xml -re vegzodo fajlt. 131 * 132 * @param directory 133 * @throws IOException 134 */ 135 public void loadConfigurations(File directory) throws ContainerException { 136 /* 137 * if (this.directory !=null) throw new 138 * ContainerException("Configuration directory already 139 * set!["+this.directory+']'); 140 */ 141 if (!directory.exists()) 142 throw new ContainerException("Directory not exists![" + directory + ']'); 143 if (!directory.isDirectory()) 144 throw new ContainerException("File is not a directory![" + directory + ']'); 145 File[] comp = listFiles(directory); 146 LOG.info("found " + comp.length + " component configurations"); 147 for (int i = 0; i < comp.length; i++) { 148 149 try { 150 ComponentConfig config = createConfig(comp[i]); 151 152 insertConfiguration(comp[i], config); 153 154 } catch (FileNotFoundException e) { 155 LOG.warn("Strange error :" + e.getMessage(), e); 156 } catch (UnsupportedEncodingException e) { 157 LOG.warn("Encoding exception, specify correct value for xml.file.encoding!", e); 158 throw new ContainerException("Encoding exception, specify correct value for xml.file.encoding!", e); 159 } 160 } 161 162 } 163 164 /*** 165 * A configuraciot be regisztralja ebbe a repositoryba, mint amit kesobb el 166 * lehet inicalizalni, ellenorzi, hogy jo neve van, s hogy a megfelelo 167 * profile-ban van e 168 * 169 * @param file 170 * @param config 171 * @return false - if component not in the current profile list 172 * @throws ContainerException 173 */ 174 private boolean insertConfiguration(File file, ComponentConfig config) throws ContainerException { 175 if (!Constants.validComponentName(config.getInstanceName())) 176 throw new ContainerException("Component name '" + config.getInstanceName() + "' is not allowed!"); 177 if (config.getProfile() != null) { 178 if (!main.hasProfile(config.getProfile())) { 179 LOG.info("Component " + config.getInstanceName() + " profile is " + config.getProfile() + ", skip"); 180 return false; 181 } 182 } 183 if (this.componentConfigMapping.containsKey(config.getInstanceName())) { 184 if (panicIfDuplicates) 185 throw new ContainerException("There is already a component named '" + config.getInstanceName() 186 + "' loaded!"); 187 LOG.warn("There is already a component named '" + config.getInstanceName() + "' loaded, skipeed"); 188 return false; 189 } 190 191 addConfig(file, config); 192 LOG.info("from " + file + " " + config.getInstanceName() + ", style:" + config.getLifeCycle()); 193 if (config.isStartupComponent()) { 194 startupComponents.add(config.getInstanceName()); 195 } 196 197 return true; 198 } 199 200 /*** 201 * @param file 202 * @return @throws 203 * UnsupportedEncodingException 204 * @throws FileNotFoundException 205 */ 206 private ComponentConfig createConfig(File file) throws UnsupportedEncodingException, FileNotFoundException, 207 StreamException, ConversionException { 208 ComponentConfig config = (ComponentConfig) main.xstream.fromXML(new InputStreamReader(new BufferedInputStream( 209 new FileInputStream(file)), xmlFileEncoding)); 210 // init configuration (create the necessary proxy class, or substitute 211 // with an other config...) 212 config.setConfigBean(configurationStrategy.initConfigurationBean(config.getConfigBean())); 213 return config; 214 } 215 216 private PersistedComponentConfig getConfigSource(File file) { 217 return (PersistedComponentConfig) fileToConfigMap.get(file.getAbsolutePath()); 218 } 219 220 /*** 221 * Check the component directory for new components 222 * 223 * @param directory 224 * @return the list of the new 'startup' components. 225 * @throws UnsupportedEncodingException 226 * @throws FileNotFoundException 227 * @throws ContainerException 228 */ 229 public List refreshComponents(File directory) throws ContainerException { 230 File[] curComp = listFiles(directory); 231 List result = new ArrayList(); 232 List newVertexList = new ArrayList(); 233 for (int i = 0; i < curComp.length; i++) { 234 try { 235 PersistedComponentConfig configSource = getConfigSource(curComp[i]); 236 if (configSource != null) { 237 // already deployed, check for reconfiguration ... 238 if (configurationStrategy.isModifiedSince(configSource.getConfig())) { 239 persistComponentConfig(configSource); 240 } else if (configSource.isModifiedSince()) { 241 // config modified in the file system 242 LOG.info("configuration modified in filesystem [" + configSource.getConfig().getInstanceName() 243 + ']'); 244 ComponentConfig newConfig = createConfig(curComp[i]); 245 // TODO: implement! 246 // 1, proxy-t generaljunk a configBean-ek koze, s itt 247 // csak szimplan lecsereljuk a benne levot 248 // 2, uj interfacet adunk a megfelelo komponensekhez, 249 // amiken keresztul ertesul a modosulasokrol 250 // 3, reflection-nel bemasoljuk a modosult property-ket 251 // az eredeti beanbe 252 253 //Object obj = 254 // main.getComponent(configSource.getConfig().getInstanceName()); 255 256 reconfigureComponent(configSource, newConfig); 257 } 258 } else { 259 ComponentConfig config = createConfig(curComp[i]); 260 if (insertConfiguration(curComp[i], config)) { 261 result.add(config.getInstanceName()); 262 newVertexList.add(vertMap.get(config.getInstanceName())); 263 } 264 } 265 } catch (FileNotFoundException e) { 266 LOG.warn("Strange error :" + e.getMessage(), e); 267 } catch (StreamException e) { 268 LOG.warn("Not well formed xml :" + e.getMessage(), e); 269 throw new ContainerException("Not well formed xml:" + e.getMessage(), e); 270 } catch (ConversionException e) { 271 LOG.warn("Not well formed xml :" + e.getMessage(), e); 272 throw new ContainerException("Not well formed xml:" + e.getMessage(), e); 273 } catch (UnsupportedEncodingException e) { 274 LOG.warn("Encoding exception, specify correct value for xml.file.encoding!", e); 275 throw new ContainerException("Encoding exception, specify correct value for xml.file.encoding!", e); 276 } 277 278 } 279 sortVertices(newVertexList); 280 return result; 281 } 282 283 /*** 284 * @param configSource 285 * @throws UnsupportedEncodingException 286 * @throws FileNotFoundException 287 */ 288 protected void persistComponentConfig(PersistedComponentConfig configSource) throws UnsupportedEncodingException, FileNotFoundException { 289 // config modified by the component 290 if (createBackups) { 291 LOG.info("configuration modified by component[" 292 + configSource.getConfig().getInstanceName() + "]: create backup"); 293 configSource.createBackup(); 294 } else { 295 LOG.info("configuration modified by component[" 296 + configSource.getConfig().getInstanceName() + "]: skip backup creation"); 297 } 298 ComponentConfig config = configSource.getConfig(); 299 // todo, ugly ... not thread safe ... 300 Object wrapped = config.getConfigBean(); 301 Object orig = configurationStrategy.getSerializableConfigurationBean(wrapped); 302 config.setConfigBean(orig); 303 saveComponentConfig(config, configSource.getFile()); 304 config.setConfigBean(wrapped); 305 306 // reset the last modified date to the current time 307 configSource.isModifiedSince(); 308 } 309 310 /*** 311 * @param configSource 312 * the persistent storage of the configuration 313 * @param newConfig 314 * the new configuration. 315 * @throws ContainerException 316 * if the component not found. Probably some concurrency error. 317 */ 318 protected void reconfigureComponent(PersistedComponentConfig configSource, ComponentConfig newConfig) 319 throws ContainerException { 320 try { 321 Object obj = main.getComponent(configSource.getConfig().getInstanceName()); 322 Object oldConfig = configSource.getConfig().getConfigBean(); 323 324 configurationStrategy.reconfigureService(obj, configSource.getConfig(), newConfig); 325 configSource.setConfig(newConfig); 326 registerComponentConfig(newConfig); 327 328 main.notifyReconfigurationListeners(obj, oldConfig, newConfig); 329 } catch (ReconfigurationFailedException rfe) { 330 LOG.warn("Reconfiguration failed:" + rfe.getMessage(), rfe); 331 } catch (ReconfigurationNotSupportedException rfe) { 332 LOG.warn("Reconfiguration failed:" + rfe.getMessage(), rfe); 333 } 334 } 335 336 /*** 337 * 338 * @param newConfig 339 */ 340 public void reconfigureComponent(ComponentConfig newConfig) { 341 try { 342 String instanceName = newConfig.getInstanceName(); 343 Object obj = main.getComponent(instanceName); 344 PersistedComponentConfig configSource = getPersistedComponentConfig(instanceName); 345 ComponentConfig oldConfig = newConfig; 346 if (configSource != null) { 347 oldConfig = configSource.getConfig(); 348 } 349 configurationStrategy.reconfigureService(obj, oldConfig, newConfig); 350 if (configSource != null) { 351 configSource.setConfig(newConfig); 352 try { 353 persistComponentConfig(configSource); 354 } catch (UnsupportedEncodingException e) { 355 LOG.warn("Reconfiguration save failed:" + e.getMessage(), e); 356 } catch (FileNotFoundException e) { 357 LOG.warn("Reconfiguration save failed:" + e.getMessage(), e); 358 } 359 } 360 registerComponentConfig(newConfig); 361 main.notifyReconfigurationListeners(obj, oldConfig.getConfigBean(), newConfig); 362 363 364 } catch (ReconfigurationFailedException rfe) { 365 LOG.warn("Reconfiguration failed:" + rfe.getMessage(), rfe); 366 } catch (ReconfigurationNotSupportedException rfe) { 367 LOG.warn("Reconfiguration failed:" + rfe.getMessage(), rfe); 368 } catch (ContainerException e) { 369 LOG.warn("Reconfiguration failed:" + e.getMessage(), e); 370 } 371 } 372 373 374 private static final class ConfigFileFilter implements FileFilter { 375 376 /* 377 * (non-Javadoc) 378 * 379 * @see java.io.FileFilter#accept(java.io.File) 380 */ 381 public boolean accept(File file) { 382 boolean acc = (file.getName().endsWith(".conf.xml")); 383 if (acc) { 384 LOG.info("component file " + file.getName()); 385 } else { 386 LOG.warn("Not a component descriptor file! ('"+file.getName()+"' not ends with '.conf.xml')"); 387 } 388 return acc; 389 } 390 } 391 392 /*** 393 * @param directory 394 * @return 395 */ 396 private File[] listFiles(File directory) { 397 return directory.listFiles(new ConfigFileFilter()); 398 } 399 400 /*** 401 * Eltarolja, a konfiguracio forrasat. 402 * 403 * @param file 404 * @param config 405 */ 406 private void addConfig(File file, ComponentConfig config) { 407 if (file != null) { 408 PersistedComponentConfig pcc = new PersistedComponentConfig(file, config); 409 fileToConfigMap.put(file.getAbsolutePath(), pcc); 410 componentToPersistedSource.put(config.getInstanceName(), pcc); 411 } 412 compConfigs.add(config); 413 registerComponentConfig(config); 414 415 Vertex v = new Vertex(config.getInstanceName(), config); 416 vertMap.put(v.getName(), v); 417 vertices.add(v); 418 419 } 420 421 private PersistedComponentConfig getPersistedComponentConfig(String instanceName) { 422 return (PersistedComponentConfig) componentToPersistedSource.get(instanceName); 423 } 424 425 /*** 426 * @param config 427 */ 428 private void registerComponentConfig(ComponentConfig config) { 429 this.componentConfigMapping.put(config.getInstanceName(), config); 430 } 431 432 /*** 433 * visszaadja a megadott nevu instancehez tartozo konfiguraciot 434 * 435 * @param instanceName 436 * @return 437 */ 438 public ComponentConfig getComponentConfig(String instanceName) { 439 return (ComponentConfig) this.componentConfigMapping.get(instanceName); 440 } 441 442 /*** 443 * sorba rendezi az osszes komponens, hogy jol inicalizalni lehessen oket. 444 * Valojaban arra kell, hogy megallapitsuk, hogy nincs benne ciklikus 445 * fuggoseg. 446 * 447 * @throws ContainerException 448 */ 449 void run() throws ContainerException { 450 sortVertices(vertices); 451 } 452 453 /*** 454 * a listaban levo Vertex -ekre megallapitja a fuggosegeket, s sorba rendezi 455 * a vertices listaba. newVerticesList reszhalmaza a vertices- nek ! 456 * 457 * @param newVerticesList 458 * az uj,sorba rendezendo komponensek listaja (Vertex- eket 459 * tartalmaz) 460 * @throws ContainerException 461 */ 462 void sortVertices(List newVerticesList) throws ContainerException { 463 // iranyitasok letrehozasa 464 assert newVerticesList != null : "list must not be null"; 465 for (int i = 0; i < newVerticesList.size(); i++) { 466 Vertex v = (Vertex) newVerticesList.get(i); 467 ComponentConfig c = (ComponentConfig) v.getNode(); 468 List deps = c.getNeededObjects(); 469 if (deps != null) { 470 for (Iterator iter = deps.iterator(); iter.hasNext();) { 471 ObjectDependency dep = (ObjectDependency) iter.next(); 472 Vertex v2 = (Vertex) vertMap.get(dep.getObjectName()); 473 if (v2 == null) 474 throw new ContainerException("No component found as '" + dep.getObjectName() 475 + "', it's required dependency of '" + c.getInstanceName() + '\''); 476 v.addDependency(v2); 477 } 478 } 479 // component with not valid name, (the built in, container provided 480 // components), do not need componentDescription 481 if (Constants.validComponentName(c.getInstanceName())) { 482 ComponentDescription desc = main.getComponentDescription(c); 483 if (desc == null) 484 throw new ContainerException("No description found for " + c.getInstanceName() + ", from class:" 485 + c.getClassAlias()); 486 if (desc.getDepends() != null) { 487 for (Iterator iter = desc.getDepends().iterator(); iter.hasNext();) { 488 ClassDependency dep = (ClassDependency) iter.next(); 489 if (dep.getDependencyType() != null) { 490 List objects = getObjectsFor(dep.getClassAlias()); 491 if (ClassDependency.AFTER.equals(dep.getDependencyType())) { 492 for (Iterator it2 = objects.iterator(); it2.hasNext();) { 493 ComponentConfig config = (ComponentConfig) it2.next(); 494 Vertex v2 = (Vertex) vertMap.get(config.getInstanceName()); 495 if (v2 == null) 496 throw new RuntimeException("it's impossible"); 497 v.addDependency(v2); 498 } 499 } 500 if (ClassDependency.BEFORE.equals(dep.getDependencyType())) { 501 for (Iterator it2 = objects.iterator(); it2.hasNext();) { 502 ComponentConfig config = (ComponentConfig) it2.next(); 503 Vertex v2 = (Vertex) vertMap.get(config.getInstanceName()); 504 if (v2 == null) 505 throw new RuntimeException("it's impossible"); 506 v2.addDependency(v); 507 } 508 } 509 if (ClassDependency.REQUIRED.equals(dep.getDependencyType())) { 510 if (objects.size() == 0) 511 throw new ContainerException("No component fullfills the required dependency '" 512 + dep.getClassAlias() + "' for '" + c.getInstanceName() + "'"); 513 } 514 515 } 516 } 517 } 518 } 519 } 520 try { 521 DirectedAcyclicGraphVerifier.topologicalSort(vertices); 522 } catch (CyclicDependencyException e) { 523 LOG.warn("Component tree is cylic!" + e.getMessage(), e); 524 525 throw new ContainerException("Component tree is cylic!" + e.getMessage(), e); 526 } 527 LOG.info("component tree is acylic, order " + Util.convertVertexList(vertices)); 528 529 } 530 531 /*** 532 * @return a list of objects which provides the specified 533 * interface/contract. 534 */ 535 public List getObjectsFor(String classAlias) { 536 ArrayList result = new ArrayList(compConfigs.size()); 537 for (Iterator iter = compConfigs.iterator(); iter.hasNext();) { 538 ComponentConfig c = (ComponentConfig) iter.next(); 539 if (classAlias.equals(c.getClassAlias())) { 540 result.add(c); 541 } else { 542 if (Constants.validComponentName(c.getInstanceName())) { 543 ComponentDescription cd = main.getComponentDescription(c); 544 if (cd == null) { 545 LOG.warn("description not found for " + c.getInstanceName() + "['" + c.getClassAlias() + "']"); 546 } else { 547 if (cd.getProvides() != null) { 548 LOG.debug("check " + cd.getClassAlias() + " if provides " + classAlias); 549 for (Iterator provides = cd.getProvides().iterator(); provides.hasNext();) { 550 String provide = (String) provides.next(); 551 if (classAlias.equals(provide)) { 552 result.add(c); 553 break; 554 } 555 } 556 } 557 } 558 } 559 } 560 } 561 return result; 562 } 563 564 /*** 565 * helper method to store the component config to a custom file 566 * 567 * @param config 568 * @param file 569 * @throws UnsupportedEncodingException 570 * @throws FileNotFoundException 571 */ 572 public void saveComponentConfig(ComponentConfig config, File file) throws UnsupportedEncodingException, 573 FileNotFoundException { 574 main.xstream.toXML(config, new OutputStreamWriter(new BufferedOutputStream(new FileOutputStream(file)), 575 xmlFileEncoding)); 576 } 577 578 public void saveConfigurations(File directory) throws ContainerException { 579 if (!directory.exists()) 580 throw new ContainerException("Directory not exists![" + directory + ']'); 581 if (!directory.isDirectory()) 582 throw new ContainerException("File is not a directory![" + directory + ']'); 583 for (Iterator iter = compConfigs.iterator(); iter.hasNext();) { 584 ComponentConfig config = (ComponentConfig) iter.next(); 585 try { 586 saveComponentConfig(config, new File(directory, config.getInstanceName() + ".conf.xml")); 587 } catch (FileNotFoundException e) { 588 LOG.warn("Strange error :" + e.getMessage(), e); 589 } catch (UnsupportedEncodingException e) { 590 LOG.warn("Encoding exception, specify correct value for xml.file.encoding!", e); 591 throw new ContainerException("Encoding exception, specify correct value for xml.file.encoding!", e); 592 } 593 } 594 } 595 596 /*** 597 * @param panicIfDuplicates 598 * The panicIfDuplicates to set. 599 */ 600 public void setPanicIfDuplicateFound(boolean panicIfDuplicates) { 601 this.panicIfDuplicates = panicIfDuplicates; 602 } 603 604 /*** 605 * @return Returns the configurationStrategy. 606 */ 607 public ConfigurationStrategy getConfigurationStrategy() { 608 return configurationStrategy; 609 } 610 611 /*** 612 * @param configurationStrategy 613 * The configurationStrategy to set. 614 */ 615 public void setConfigurationStrategy(ConfigurationStrategy configurationStrategy) { 616 this.configurationStrategy = configurationStrategy; 617 } 618 619 /*** 620 * @param createBackups 621 * The createBackups to set. 622 */ 623 public void setCreateBackups(boolean createBackups) { 624 this.createBackups = createBackups; 625 } 626 627 /*** 628 * @return Returns the createBackups. 629 */ 630 public boolean isCreateBackups() { 631 return createBackups; 632 } 633 }

This page was automatically generated by Maven