001/* 002 * Copyright 2007 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kualigan.maven.plugins.api; 017 018import java.io.DataInputStream; 019import java.io.File; 020import java.io.FileOutputStream; 021import java.io.IOException; 022import java.io.InputStream; 023import java.util.ArrayList; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.Properties; 028import java.util.StringTokenizer; 029 030import org.apache.commons.cli.CommandLine; 031import org.apache.commons.cli.Options; 032import org.apache.commons.cli.PosixParser; 033import org.apache.commons.exec.DefaultExecutor; 034import org.apache.commons.exec.ExecuteException; 035import org.apache.commons.exec.Executor; 036import org.apache.commons.exec.PumpStreamHandler; 037import org.apache.maven.archetype.Archetype; 038import org.apache.maven.artifact.repository.ArtifactRepositoryFactory; 039import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout; 040import org.apache.maven.plugin.AbstractMojo; 041import org.apache.maven.plugin.MojoExecutionException; 042import org.apache.maven.shared.invoker.DefaultInvocationRequest; 043import org.apache.maven.shared.invoker.DefaultInvoker; 044import org.apache.maven.shared.invoker.InvocationRequest; 045import org.apache.maven.shared.invoker.InvocationResult; 046import org.apache.maven.shared.invoker.Invoker; 047import org.apache.maven.shared.invoker.MavenInvocationException; 048import org.codehaus.plexus.archiver.Archiver; 049import org.codehaus.plexus.archiver.ArchiverException; 050import org.codehaus.plexus.archiver.UnArchiver; 051import org.codehaus.plexus.archiver.manager.ArchiverManager; 052import org.codehaus.plexus.archiver.manager.NoSuchArchiverException; 053import org.codehaus.plexus.archiver.util.DefaultFileSet; 054import org.codehaus.plexus.archiver.zip.ZipArchiver; 055import org.codehaus.plexus.component.annotations.Component; 056import org.codehaus.plexus.component.annotations.Requirement; 057import org.codehaus.plexus.components.interactivity.Prompter; 058import org.codehaus.plexus.components.io.fileselectors.IncludeExcludeFileSelector; 059import org.codehaus.plexus.util.FileUtils; 060import org.codehaus.plexus.util.StringUtils; 061import org.codehaus.plexus.util.cli.CommandLineUtils; 062 063/** 064 * Creates a prototype from the given KFS project resource. A KFS project resource can be either 065 * of the following: 066 * <ul> 067 * <li>KFS war file</li> 068 * <li>KFS project directory with source</li> 069 * <li>KFS svn repo</li> 070 * </ul> 071 * 072 * @author Leo Przybylski 073 */ 074@Component(role = org.kualigan.maven.plugins.api.PrototypeHelper.class, hint="default") 075public class DefaultPrototypeHelper implements PrototypeHelper { 076 public static final String ROLE_HINT = "default"; 077 078 private static final Options OPTIONS = new Options(); 079 080 private static final char SET_SYSTEM_PROPERTY = 'D'; 081 082 private static final char OFFLINE = 'o'; 083 084 private static final char REACTOR = 'r'; 085 086 private static final char QUIET = 'q'; 087 088 private static final char DEBUG = 'X'; 089 090 private static final char ERRORS = 'e'; 091 092 private static final char NON_RECURSIVE = 'N'; 093 094 private static final char UPDATE_SNAPSHOTS = 'U'; 095 096 private static final char ACTIVATE_PROFILES = 'P'; 097 098 private static final String FORCE_PLUGIN_UPDATES = "cpu"; 099 100 private static final String FORCE_PLUGIN_UPDATES2 = "up"; 101 102 private static final String SUPPRESS_PLUGIN_UPDATES = "npu"; 103 104 private static final String SUPPRESS_PLUGIN_REGISTRY = "npr"; 105 106 private static final char CHECKSUM_FAILURE_POLICY = 'C'; 107 108 private static final char CHECKSUM_WARNING_POLICY = 'c'; 109 110 private static final char ALTERNATE_USER_SETTINGS = 's'; 111 112 private static final String FAIL_FAST = "ff"; 113 114 private static final String FAIL_AT_END = "fae"; 115 116 private static final String FAIL_NEVER = "fn"; 117 118 private static final String ALTERNATE_POM_FILE = "f"; 119 120 /** 121 */ 122 @Requirement 123 protected Archetype archetype; 124 125 /** 126 */ 127 @Requirement 128 protected Prompter prompter; 129 130 /** 131 */ 132 @Requirement 133 protected ArtifactRepositoryFactory artifactRepositoryFactory; 134 135 /** 136 */ 137 @Requirement(role=org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout.class, hint="default") 138 protected ArtifactRepositoryLayout defaultArtifactRepositoryLayout; 139 140 @Requirement 141 protected ArchiverManager archiverManager; 142 143 private AbstractMojo caller; 144 145 protected void logUnpack(final File file, final File location, final String includes, final String excludes ) { 146 if (!getCaller().getLog().isInfoEnabled()) { 147 return; 148 } 149 150 StringBuffer msg = new StringBuffer(); 151 msg.append( "Unpacking " ); 152 msg.append( file ); 153 msg.append( " to " ); 154 msg.append( location ); 155 156 if ( includes != null && excludes != null ) { 157 msg.append( " with includes \"" ); 158 msg.append( includes ); 159 msg.append( "\" and excludes \"" ); 160 msg.append( excludes ); 161 msg.append( "\"" ); 162 } 163 else if (includes != null) { 164 msg.append( " with includes \"" ); 165 msg.append( includes ); 166 msg.append( "\"" ); 167 } 168 else if (excludes != null) { 169 msg.append( " with excludes \"" ); 170 msg.append( excludes ); 171 msg.append( "\"" ); 172 } 173 174 getCaller().getLog().info(msg.toString()); 175 } 176 177 178 protected int executeCommandLine(final String ... args) throws ExecuteException, IOException { 179 final Executor exec = new DefaultExecutor(); 180 final Map enviro = new HashMap(); 181 try { 182 final Properties systemEnvVars = CommandLineUtils.getSystemEnvVars(); 183 enviro.putAll(systemEnvVars); 184 } catch (IOException x) { 185 getCaller().getLog().error("Could not assign default system enviroment variables.", x); 186 } 187 188 final File workingDirectory = new File(System.getProperty("java.io.tmpdir")); 189 final org.apache.commons.exec.CommandLine commandLine = new org.apache.commons.exec.CommandLine(args[0]); 190 for (int i = 1; i < args.length; i++) { 191 commandLine.addArgument(args[i]); 192 } 193 194 exec.setStreamHandler(new PumpStreamHandler(System.out, System.err, System.in)); 195 exec.setWorkingDirectory(workingDirectory); 196 return exec.execute(commandLine, enviro); 197 } 198 199 /** 200 * Handles repacking of the war as a jar file with classes, etc... Basically makes a jar of everything 201 * in the war file's WEB-INF/classes path. 202 * 203 * @param file is the war file to repackage 204 * @return {@link File} instance of the repacked jar 205 */ 206 public File repack(final File file, final String artifactId) throws MojoExecutionException { 207 final File workingDirectory = new File(System.getProperty("java.io.tmpdir") + File.separator 208 + artifactId + "-repack"); 209 final File warDirectory = new File(workingDirectory, "war"); 210 final File repackDirectory = new File(workingDirectory, artifactId); 211 final File classesDirectory = new File(warDirectory, "WEB-INF/classes"); 212 final File retval = new File(workingDirectory, artifactId + ".jar"); 213 214 try { 215 workingDirectory.mkdirs(); 216 workingDirectory.mkdir(); 217 workingDirectory.deleteOnExit(); 218 warDirectory.mkdir(); 219 } 220 catch (Exception e) { 221 throw new MojoExecutionException("Unable to create working directory for repackaging", e); 222 } 223 224 unpack(file, warDirectory, "**/classes/**", null); 225 226 try { 227 FileUtils.copyDirectoryStructure(classesDirectory, repackDirectory); 228 } 229 catch (Exception e) { 230 throw new MojoExecutionException("Unable to copy files into the repack directory"); 231 } 232 233 try { 234 pack(retval, repackDirectory, "**/**", null); 235 } 236 catch (Exception e) { 237 throw new MojoExecutionException("Was unable to create the jar", e); 238 } 239 240 return retval; 241 } 242 243 /** 244 * Unpacks the archive file. 245 * 246 * @param file File to be unpacked. 247 * @param location Location where to put the unpacked files. 248 * @param includes Comma separated list of file patterns to include i.e. <code>**/.xml, 249 * **/*.properties</code> 250 * @param excludes Comma separated list of file patterns to exclude i.e. <code>**/*.xml, 251 * **/*.properties</code> 252 */ 253 protected void unpack(final File file, final File location, final String includes, final String excludes) throws MojoExecutionException { 254 try { 255 logUnpack(file, location, includes, excludes); 256 257 location.mkdirs(); 258 259 final UnArchiver unArchiver; 260 unArchiver = archiverManager.getUnArchiver(file); 261 unArchiver.setSourceFile(file); 262 unArchiver.setDestDirectory(location); 263 264 if (StringUtils.isNotEmpty(excludes) || StringUtils.isNotEmpty(includes)) { 265 final IncludeExcludeFileSelector[] selectors = 266 new IncludeExcludeFileSelector[]{ new IncludeExcludeFileSelector() }; 267 268 if (StringUtils.isNotEmpty( excludes )) { 269 selectors[0].setExcludes(excludes.split( "," )); 270 } 271 272 if (StringUtils.isNotEmpty( includes )) { 273 selectors[0].setIncludes(includes.split( "," )); 274 } 275 276 unArchiver.setFileSelectors(selectors); 277 } 278 279 unArchiver.extract(); 280 } 281 catch ( NoSuchArchiverException e ) { 282 throw new MojoExecutionException("Unknown archiver type", e); 283 } 284 catch (ArchiverException e) { 285 e.printStackTrace(); 286 throw new MojoExecutionException( 287 "Error unpacking file: " + file + " to: " + location + "\r\n" + e.toString(), e ); 288 } 289 } 290 291 /** 292 * Packs a jar 293 * 294 * @param file Destination file. 295 * @param location Directory source. 296 * @param includes Comma separated list of file patterns to include i.e. <code>**/.xml, 297 * **/*.properties</code> 298 * @param excludes Comma separated list of file patterns to exclude i.e. <code>**/*.xml, 299 * **/*.properties</code> 300 */ 301 protected void pack(final File file, final File location, final String includes, final String excludes) throws MojoExecutionException { 302 try { 303 304 final Archiver archiver; 305 archiver = archiverManager.getArchiver(file); 306 archiver.addFileSet(new DefaultFileSet() {{ 307 setDirectory(location); 308 if (includes != null) { 309 setIncludes(includes.split(",")); 310 } 311 if (excludes != null) { 312 setExcludes(excludes.split(",")); 313 } 314 }}); 315 archiver.setDestFile(file); 316 317 archiver.createArchive(); 318 } 319 catch ( NoSuchArchiverException e ) { 320 throw new MojoExecutionException("Unknown archiver type", e); 321 } 322 catch (Exception e) { 323 e.printStackTrace(); 324 throw new MojoExecutionException( 325 "Error packing directory: " + location + " to: " + file + "\r\n" + e.toString(), e ); 326 } 327 } 328 329 /** 330 * 331 */ 332 private void setupRequest(final InvocationRequest req, 333 final String additionalArguments) throws MojoExecutionException { 334 try { 335 final String[] args = CommandLineUtils.translateCommandline(additionalArguments); 336 CommandLine cli = new PosixParser().parse(OPTIONS, args); 337 338 if (cli.hasOption( SET_SYSTEM_PROPERTY)) 339 { 340 String[] properties = cli.getOptionValues( SET_SYSTEM_PROPERTY ); 341 Properties props = new Properties(); 342 for ( int i = 0; i < properties.length; i++ ) 343 { 344 String property = properties[i]; 345 String name, value; 346 int sep = property.indexOf( "=" ); 347 if ( sep <= 0 ) 348 { 349 name = property.trim(); 350 value = "true"; 351 } 352 else 353 { 354 name = property.substring( 0, sep ).trim(); 355 value = property.substring( sep + 1 ).trim(); 356 } 357 props.setProperty( name, value ); 358 } 359 360 req.setProperties( props ); 361 } 362 363 if ( cli.hasOption( OFFLINE ) ) 364 { 365 req.setOffline( true ); 366 } 367 368 if ( cli.hasOption( QUIET ) ) 369 { 370 // TODO: setQuiet() currently not supported by InvocationRequest 371 req.setDebug( false ); 372 } 373 else if ( cli.hasOption( DEBUG ) ) 374 { 375 req.setDebug( true ); 376 } 377 else if ( cli.hasOption( ERRORS ) ) 378 { 379 req.setShowErrors( true ); 380 } 381 382 if ( cli.hasOption( REACTOR ) ) 383 { 384 req.setRecursive( true ); 385 } 386 else if ( cli.hasOption( NON_RECURSIVE ) ) 387 { 388 req.setRecursive( false ); 389 } 390 391 if ( cli.hasOption( UPDATE_SNAPSHOTS ) ) 392 { 393 req.setUpdateSnapshots( true ); 394 } 395 396 if ( cli.hasOption( ACTIVATE_PROFILES ) ) 397 { 398 String[] profiles = cli.getOptionValues( ACTIVATE_PROFILES ); 399 List<String> activatedProfiles = new ArrayList<String>(); 400 List<String> deactivatedProfiles = new ArrayList<String>(); 401 402 if ( profiles != null ) 403 { 404 for ( int i = 0; i < profiles.length; ++i ) 405 { 406 StringTokenizer profileTokens = new StringTokenizer( profiles[i], "," ); 407 408 while ( profileTokens.hasMoreTokens() ) 409 { 410 String profileAction = profileTokens.nextToken().trim(); 411 412 if ( profileAction.startsWith( "-" ) || profileAction.startsWith( "!" ) ) 413 { 414 deactivatedProfiles.add( profileAction.substring( 1 ) ); 415 } 416 else if ( profileAction.startsWith( "+" ) ) 417 { 418 activatedProfiles.add( profileAction.substring( 1 ) ); 419 } 420 else 421 { 422 activatedProfiles.add( profileAction ); 423 } 424 } 425 } 426 } 427 428 if (!deactivatedProfiles.isEmpty()) { 429 getCaller().getLog().warn("Explicit profile deactivation is not yet supported. " 430 + "The following profiles will NOT be deactivated: " + StringUtils.join( 431 deactivatedProfiles.iterator(), ", ")); 432 } 433 434 if (!activatedProfiles.isEmpty()) { 435 req.setProfiles(activatedProfiles); 436 } 437 } 438 439 if (cli.hasOption(FORCE_PLUGIN_UPDATES) || cli.hasOption( FORCE_PLUGIN_UPDATES2)) { 440 getCaller().getLog().warn("Forcing plugin updates is not supported currently."); 441 } 442 else if (cli.hasOption( SUPPRESS_PLUGIN_UPDATES)) { 443 req.setNonPluginUpdates( true ); 444 } 445 446 if (cli.hasOption( SUPPRESS_PLUGIN_REGISTRY)) { 447 getCaller().getLog().warn("Explicit suppression of the plugin registry is not supported currently." ); 448 } 449 450 if (cli.hasOption(CHECKSUM_FAILURE_POLICY)) { 451 req.setGlobalChecksumPolicy( InvocationRequest.CHECKSUM_POLICY_FAIL ); 452 } 453 else if ( cli.hasOption( CHECKSUM_WARNING_POLICY ) ) 454 { 455 req.setGlobalChecksumPolicy( InvocationRequest.CHECKSUM_POLICY_WARN ); 456 } 457 458 if ( cli.hasOption( ALTERNATE_USER_SETTINGS ) ) 459 { 460 req.setUserSettingsFile( new File( cli.getOptionValue( ALTERNATE_USER_SETTINGS ) ) ); 461 } 462 463 if ( cli.hasOption( FAIL_AT_END ) ) 464 { 465 req.setFailureBehavior( InvocationRequest.REACTOR_FAIL_AT_END ); 466 } 467 else if ( cli.hasOption( FAIL_FAST ) ) 468 { 469 req.setFailureBehavior( InvocationRequest.REACTOR_FAIL_FAST ); 470 } 471 if ( cli.hasOption( FAIL_NEVER ) ) 472 { 473 req.setFailureBehavior( InvocationRequest.REACTOR_FAIL_NEVER ); 474 } 475 if ( cli.hasOption( ALTERNATE_POM_FILE ) ) 476 { 477 if (req.getPomFileName() != null) { 478 getCaller().getLog().info("pomFileName is already set, ignoring the -f argument" ); 479 } 480 else { 481 req.setPomFileName( cli.getOptionValue( ALTERNATE_POM_FILE ) ); 482 } 483 } 484 } 485 catch ( Exception e ) { 486 throw new MojoExecutionException("Failed to re-parse additional arguments for Maven invocation.", e ); 487 } 488 } 489 490 /** 491 * Executes the {@code install-file} goal with the new pom against the artifact file. 492 * 493 * @param artifact {@link File} instance to install 494 */ 495 public void installArtifact(final File artifact, 496 final File sources, 497 final File mavenHome, 498 final String groupId, 499 final String artifactId, 500 final String version, 501 final String repositoryId) throws MojoExecutionException { 502 extractBuildXml(); 503 504 filterTempPom(groupId, artifactId, artifact.getName().endsWith("jar") ? "jar" : "war", version); 505 506 final Invoker invoker = new DefaultInvoker().setMavenHome(mavenHome); 507 508 final String additionalArguments = ""; 509 510 getCaller().getLog().debug("Setting up properties for installing the artifact"); 511 final InvocationRequest req = new DefaultInvocationRequest() 512 .setInteractive(false) 513 .setProperties(new Properties() {{ 514 setProperty("pomFile", getTempPomPath()); 515 if (repositoryId != null) { 516 setProperty("repositoryId", repositoryId); 517 } 518 if (sources != null) { 519 try { 520 setProperty("sources", zipSourcesIfRequired(sources).getCanonicalPath()); 521 } 522 catch (Exception e) { 523 throw new MojoExecutionException("Cannot get path for the sources file ", e); 524 } 525 } 526 try { 527 setProperty("file", artifact.getCanonicalPath()); 528 } 529 catch (Exception e) { 530 throw new MojoExecutionException("Cannot get path for the war file ", e); 531 } 532 setProperty("updateReleaseInfo", "true"); 533 }}); 534 535 getCaller().getLog().debug("Properties used for installArtifact are:"); 536 try { 537 req.getProperties().list(System.out); 538 } 539 catch (Exception e) { 540 } 541 542 try { 543 setupRequest(req, additionalArguments); 544 545 if (repositoryId == null) { 546 req.setGoals(new ArrayList<String>() {{ add("install:install-file"); }}); 547 } 548 else { 549 req.setGoals(new ArrayList<String>() {{ add("deploy:deploy-file"); }}); 550 } 551 552 try { 553 final InvocationResult invocationResult = invoker.execute(req); 554 555 if ( invocationResult.getExecutionException() != null ) { 556 throw new MojoExecutionException("Error executing Maven.", 557 invocationResult.getExecutionException()); 558 } 559 560 if (invocationResult.getExitCode() != 0) { 561 throw new MojoExecutionException( 562 "Maven execution failed, exit code: \'" + invocationResult.getExitCode() + "\'"); 563 } 564 } 565 catch (MavenInvocationException e) { 566 throw new MojoExecutionException( "Failed to invoke Maven build.", e ); 567 } 568 } 569 finally { 570 /* 571 if ( settingsFile != null && settingsFile.exists() && !settingsFile.delete() ) 572 { 573 settingsFile.deleteOnExit(); 574 } 575 */ 576 } 577 } 578 579 /** 580 * Executes the {@code install-file} goal with the new pom against the artifact file. 581 * 582 * @param artifact {@link File} instance to install 583 */ 584 public void filterTempPom(final String groupId, 585 final String artifactId, 586 final String packaging, 587 final String version) throws MojoExecutionException { 588 getCaller().getLog().info("Extracting the Temp POM"); 589 590 try { 591 executeCommandLine("ant", 592 "-Dsource=" + System.getProperty("java.io.tmpdir") + File.separator + "pom.xml", 593 "-Dtarget=" + System.getProperty("java.io.tmpdir") + File.separator + "prototype-pom.xml", 594 "-DgroupId=" + groupId, 595 "-DartifactId=" + artifactId, 596 "-Dpackaging=" + packaging, 597 "-Dversion=" + version); 598 } 599 catch (Exception e) { 600 throw new MojoExecutionException("Error trying to filter the pom with ant ", e); 601 } 602 } 603 604 /** 605 * Temporary POM location 606 * 607 * @return String value the path of the temporary POM 608 */ 609 protected String getTempPomPath() { 610 return System.getProperty("java.io.tmpdir") + File.separator + "prototype-pom.xml"; 611 } 612 613 /** 614 * Puts ant build file in the system temp directory. build.xml is extracted 615 * from the plugin. 616 */ 617 public void extractBuildXml() throws MojoExecutionException { 618 getCaller().getLog().info("Extracting the build.xml"); 619 620 final InputStream pom_is = getClass().getClassLoader().getResourceAsStream("prototype-resources/build.xml"); 621 622 byte[] fileBytes = null; 623 try { 624 final DataInputStream dis = new DataInputStream(pom_is); 625 fileBytes = new byte[dis.available()]; 626 dis.readFully(fileBytes); 627 dis.close(); 628 } 629 catch (Exception e) { 630 throw new MojoExecutionException("Wasn't able to read in the prototype pom", e); 631 } 632 finally { 633 try { 634 pom_is.close(); 635 } 636 catch (Exception e) { 637 // Ignore exceptions 638 } 639 } 640 641 try { 642 final FileOutputStream fos = new FileOutputStream(System.getProperty("java.io.tmpdir") + File.separator + "build.xml"); 643 try { 644 fos.write(fileBytes); 645 } 646 finally { 647 fos.close(); 648 } 649 } 650 catch (Exception e) { 651 throw new MojoExecutionException("Could not write temporary pom file", e); 652 } 653 } 654 655 /** 656 * Puts temporary pom in the system temp directory. prototype-pom.xml is extracted 657 * from the plugin. 658 */ 659 public void extractTempPom() throws MojoExecutionException { 660 getCaller().getLog().info("Extracting the Temp Pom"); 661 662 final InputStream pom_is = getClass().getClassLoader().getResourceAsStream("prototype-resources/pom.xml"); 663 664 byte[] fileBytes = null; 665 try { 666 final DataInputStream dis = new DataInputStream(pom_is); 667 fileBytes = new byte[dis.available()]; 668 dis.readFully(fileBytes); 669 dis.close(); 670 } 671 catch (Exception e) { 672 throw new MojoExecutionException("Wasn't able to read in the prototype pom", e); 673 } 674 finally { 675 try { 676 pom_is.close(); 677 } 678 catch (Exception e) { 679 // Ignore exceptions 680 } 681 } 682 683 try { 684 final FileOutputStream fos = new FileOutputStream(System.getProperty("java.io.tmpdir") + File.separator + "pom.xml"); 685 try { 686 fos.write(fileBytes); 687 } 688 finally { 689 fos.close(); 690 } 691 } 692 catch (Exception e) { 693 throw new MojoExecutionException("Could not write temporary pom file", e); 694 } 695 } 696 697 protected File zipSourcesIfRequired(File sources) { 698 if (sources.isFile()){ 699 //Already a zip 700 return sources; 701 } 702 final File zipFile = new File(System.getProperty("java.io.tmpdir") + File.separator + "sources.zip"); 703 zipFile.deleteOnExit(); 704 ZipArchiver zipArchiver = new ZipArchiver(); 705 zipArchiver.addDirectory(sources, new String[]{"**/*"}, new String[]{}); 706 zipArchiver.setDestFile(zipFile); 707 try { 708 zipArchiver.createArchive(); 709 } catch (IOException e) { 710 throw new RuntimeException("Unable to zip source directory", e); 711 } 712 return zipFile; 713 } 714 715 public void setCaller(final AbstractMojo caller) { 716 this.caller = caller; 717 } 718 719 public AbstractMojo getCaller() { 720 return this.caller; 721 } 722 723}