InlineMojo.java

  1. /*
  2.  * Licensed under the Apache License, Version 2.0 (the "License");
  3.  * you may not use this file except in compliance with the License.
  4.  * You may obtain a copy of the License at
  5.  *
  6.  * http://www.apache.org/licenses/LICENSE-2.0
  7.  *
  8.  * Unless required by applicable law or agreed to in writing, software
  9.  * distributed under the License is distributed on an "AS IS" BASIS,
  10.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11.  * See the License for the specific language governing permissions and
  12.  * limitations under the License.
  13.  */

  14. package org.basepom.inline.mojo;

  15. import static com.google.common.base.Preconditions.checkState;
  16. import static java.lang.String.format;
  17. import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;
  18. import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

  19. import org.basepom.inline.transformer.ClassPath;
  20. import org.basepom.inline.transformer.ClassPathResource;
  21. import org.basepom.inline.transformer.ClassPathTag;
  22. import org.basepom.inline.transformer.JarTransformer;
  23. import org.basepom.inline.transformer.TransformerException;

  24. import java.io.BufferedReader;
  25. import java.io.BufferedWriter;
  26. import java.io.File;
  27. import java.io.IOException;
  28. import java.io.UncheckedIOException;
  29. import java.nio.charset.StandardCharsets;
  30. import java.nio.file.Files;
  31. import java.util.List;
  32. import java.util.Map;
  33. import java.util.Optional;
  34. import java.util.Set;
  35. import java.util.function.BiConsumer;
  36. import java.util.function.Consumer;
  37. import java.util.function.Predicate;
  38. import java.util.jar.JarEntry;
  39. import java.util.jar.JarOutputStream;
  40. import java.util.stream.Collectors;
  41. import javax.xml.stream.XMLStreamException;

  42. import com.google.common.base.Functions;
  43. import com.google.common.base.Joiner;
  44. import com.google.common.collect.ImmutableList;
  45. import com.google.common.collect.ImmutableMap;
  46. import com.google.common.collect.ImmutableSet;
  47. import com.google.common.collect.ImmutableSetMultimap;
  48. import com.google.common.collect.ImmutableSortedSet;
  49. import com.google.common.collect.Iterables;
  50. import com.google.common.collect.Sets;
  51. import com.google.common.io.CharStreams;
  52. import org.apache.maven.execution.MavenSession;
  53. import org.apache.maven.plugin.AbstractMojo;
  54. import org.apache.maven.plugin.MojoExecutionException;
  55. import org.apache.maven.plugins.annotations.Component;
  56. import org.apache.maven.plugins.annotations.LifecyclePhase;
  57. import org.apache.maven.plugins.annotations.Mojo;
  58. import org.apache.maven.plugins.annotations.Parameter;
  59. import org.apache.maven.plugins.annotations.ResolutionScope;
  60. import org.apache.maven.project.DependencyResolutionException;
  61. import org.apache.maven.project.MavenProject;
  62. import org.apache.maven.project.MavenProjectHelper;
  63. import org.apache.maven.project.ProjectBuilder;
  64. import org.apache.maven.project.ProjectBuildingException;
  65. import org.apache.maven.project.ProjectDependenciesResolver;
  66. import org.eclipse.aether.RepositorySystem;
  67. import org.eclipse.aether.artifact.Artifact;
  68. import org.eclipse.aether.graph.Dependency;
  69. import org.eclipse.aether.util.artifact.JavaScopes;
  70. import org.jdom2.JDOMException;

  71. /**
  72.  * Inlines one or more dependencies of the project, relocated the classes and writes a new artifact.
  73.  */
  74. @Mojo(name = "inline", defaultPhase = LifecyclePhase.PACKAGE,
  75.         requiresProject = true, threadSafe = true,
  76.         requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME,
  77.         requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
  78. public final class InlineMojo extends AbstractMojo {

  79.     private static final PluginLog LOG = new PluginLog(InlineMojo.class);

  80.     private static final Predicate<Dependency> EXCLUDE_SYSTEM_SCOPE = dependency -> !JavaScopes.SYSTEM.equals(dependency.getScope());
  81.     private static final Predicate<Dependency> EXCLUDE_PROVIDED_SCOPE = dependency -> !JavaScopes.PROVIDED.equals(dependency.getScope());


  82.     @Parameter(defaultValue = "${project}", readonly = true, required = true)
  83.     private MavenProject project;

  84.     @Parameter(defaultValue = "${session}", readonly = true, required = true)
  85.     private MavenSession mavenSession;

  86.     @Parameter(defaultValue = "${reactorProjects}", readonly = true, required = true)
  87.     private List<MavenProject> reactorProjects;

  88.     @Component
  89.     private ProjectBuilder mavenProjectBuilder;

  90.     @Component
  91.     private ProjectDependenciesResolver projectDependenciesResolver;

  92.     @Component
  93.     private RepositorySystem repositorySystem;

  94.     @Component
  95.     private MavenProjectHelper projectHelper;

  96.     /**
  97.      * The destination directory for the inlined artifact.
  98.      */
  99.     @Parameter(defaultValue = "${project.build.directory}")
  100.     private File outputDirectory;

  101.     /**
  102.      * The POM file to use.
  103.      */
  104.     @Parameter(property = "inline.pomFile", defaultValue = "${project.file}")
  105.     private File pomFile;

  106.     /**
  107.      * Direct dependencies to inline. Each dependency here must be
  108.      * listed in the project POM. Any transitive dependency is added
  109.      * to the final jar, unless it is in {@code RUNTIME} scope.
  110.      * {@code RUNTIME} dependencies become a runtime dependency of the
  111.      * resulting final jar <b>unless</b> they are listed here. In that
  112.      * case, they are inlined in the final jar as well.
  113.      */
  114.     @Parameter
  115.     private List<InlineDependency> inlineDependencies = ImmutableList.of();

  116.     // called by maven
  117.     public void setInlineDependencies(List<InlineDependency> inlineDependencies) {
  118.         this.inlineDependencies = ImmutableList.copyOf(inlineDependencies);
  119.     }

  120.     /**
  121.      * Include dependencies. A dependency is given as <tt>groupId:artifactId</tt>. The wildcard character '*' is supported for group id and artifact id.
  122.      * <p>
  123.      * Includes and excludes operate on the list of potential dependencies to inline. They can not be used to add additional dependencies that are not
  124.      * listed in the &lt;inlineDependency&gt; elements.
  125.      */
  126.     @Parameter
  127.     private List<ArtifactIdentifier> includes = ImmutableList.of();

  128.     // called by maven
  129.     public void setIncludes(List<String> includes) {
  130.         this.includes = includes.stream().map(ArtifactIdentifier::new).collect(Collectors.toList());
  131.     }

  132.     /**
  133.      * Exclude dependencies from inclusion. A dependency is given as <tt>groupId:artifactId</tt>. Any transitive dependency that has been pulled in can be
  134.      * excluded here. The wildcard character '*' is supported for group id and artifact id.
  135.      * <p>
  136.      * Includes and excludes operate on the list of potential dependencies to inline. They can not be used to add additional dependencies that are not
  137.      * listed in the &lt;inlineDependency&gt; elements.
  138.      */
  139.     @Parameter
  140.     private List<ArtifactIdentifier> excludes = ImmutableList.of();

  141.     // called by maven
  142.     public void setExcludes(List<String> excludes) {
  143.         this.excludes = excludes.stream().map(ArtifactIdentifier::new).collect(Collectors.toList());
  144.     }

  145.     /**
  146.      * Adds external jar processors. These must be on the dependency path for the plugin. See the "Additional Processors" documentation.
  147.      */
  148.     @Parameter
  149.     private List<String> additionalProcessors = ImmutableList.of();

  150.     // called by maven
  151.     public void setAdditionalProcessors(List<String> processors) {
  152.         this.additionalProcessors = ImmutableList.copyOf(processors);
  153.     }

  154.     /**
  155.      * Hide inlined classes from IDE autocompletion.
  156.      */
  157.     @Parameter(defaultValue = "true", property = "inline.hide-classes")
  158.     private boolean hideClasses;

  159.     /**
  160.      * Skip the execution.
  161.      */
  162.     @Parameter(defaultValue = "false", property = "inline.skip")
  163.     private boolean skip;

  164.     /**
  165.      * Silence all non-output and non-error messages.
  166.      */
  167.     @Parameter(defaultValue = "false", property = "inline.quiet")
  168.     private boolean quiet;

  169.     /**
  170.      * Defines the package prefix for all relocated classes. This prefix must be a valid package name. All relocated classes are put under this prefix.
  171.      */
  172.     @Parameter(required = true, property = "inline.prefix")
  173.     private String prefix;

  174.     /**
  175.      * Fail if an inline dependency is defined but the corresponding dependency is not actually found.
  176.      */
  177.     @Parameter(defaultValue = "true", property = "inline.failOnNoMatch")
  178.     private boolean failOnNoMatch;

  179.     /**
  180.      * Fail if any duplicate exists after processing the contents.
  181.      */
  182.     @Parameter(defaultValue = "true", property = "inline.failOnDuplicate")
  183.     private boolean failOnDuplicate;

  184.     /**
  185.      * The path to the output file for the inlined artifact. When this parameter is set, the created archive will neither replace the project's main artifact
  186.      * nor will it be attached. Hence, this parameter causes the parameters {@link #inlinedArtifactAttached}, {@link #inlinedClassifierName} to be ignored when
  187.      * used.
  188.      */
  189.     @Parameter
  190.     private File outputJarFile;

  191.     /**
  192.      * The path to the output file for the new POM file. When this parameter is set, the created pom file will not replace the project's pom file.
  193.      */
  194.     @Parameter
  195.     private File outputPomFile;


  196.     /**
  197.      * If true, attach the inlined artifact, if false replace the original artifact.
  198.      */
  199.     @Parameter(defaultValue = "false", property = "inline.attachArtifact")
  200.     private boolean inlinedArtifactAttached;

  201.     /**
  202.      * If true, replace the POM file with a new version that has all inlined dependencies removed. It is possible to write a POM file that works to build the
  203.      * jar with inlined dependencies and then use the same POM file for the resulting artifact (by having all dependencies marked as <tt>provided</tt> and
  204.      * ensure that those dependencies do not have additional, transitive dependencies. This tends to be error prone and it is recommended to have the plugin
  205.      * rewrite the POM file.
  206.      */
  207.     @Parameter(defaultValue = "true", property = "inline.replacePomFile")
  208.     private boolean replacePomFile;

  209.     /**
  210.      * The name of the classifier used in case the inlined artifact is attached.
  211.      */
  212.     @Parameter(defaultValue = "inlined")
  213.     private String inlinedClassifierName;


  214.     @Override
  215.     public void execute() throws MojoExecutionException {

  216.         if (this.skip) {
  217.             LOG.report(quiet, "skipping plugin execution");
  218.             return;
  219.         }

  220.         if ("pom".equals(project.getPackaging())) {
  221.             LOG.report(quiet, "ignoring POM project");
  222.             return;
  223.         }

  224.         if (project.getArtifact().getFile() == null) {
  225.             throw new MojoExecutionException("No project artifact found!");
  226.         }

  227.         try {
  228.             ImmutableSetMultimap.Builder<InlineDependency, Dependency> dependencyBuilder = ImmutableSetMultimap.builder();
  229.             ImmutableSet.Builder<Dependency> pomDependenciesToAdd = ImmutableSet.builder();

  230.             computeDependencyMap(dependencyBuilder, pomDependenciesToAdd);

  231.             ImmutableSetMultimap<InlineDependency, Dependency> dependencyMap = dependencyBuilder.build();

  232.             rewriteJarFile(dependencyMap);
  233.             rewritePomFile(pomDependenciesToAdd.build(), ImmutableSet.copyOf(dependencyMap.values()));

  234.         } catch (UncheckedIOException e) {
  235.             throw new MojoExecutionException(e.getCause());
  236.         } catch (TransformerException | IOException | DependencyResolutionException | ProjectBuildingException | XMLStreamException | JDOMException e) {
  237.             throw new MojoExecutionException(e);
  238.         }
  239.     }

  240.     private void computeDependencyMap(
  241.             ImmutableSetMultimap.Builder<InlineDependency, Dependency> dependencyMapBuilder,
  242.             ImmutableSet.Builder<Dependency> pomBuilder)
  243.             throws DependencyResolutionException, ProjectBuildingException {

  244.         DependencyBuilder dependencyBuilder = new DependencyBuilder(project, mavenSession, mavenProjectBuilder, projectDependenciesResolver, reactorProjects);

  245.         ImmutableSet<ArtifactIdentifier> directArtifacts = project.getDependencyArtifacts().stream()
  246.                 .map(ArtifactIdentifier::new)
  247.                 .collect(ImmutableSet.toImmutableSet());

  248.         ImmutableList<Dependency> directDependencies = dependencyBuilder.mapProject(project,
  249.                 (node, parents) -> directArtifacts.contains(new ArtifactIdentifier(node)));

  250.         // build the full set of dependencies with all scopes and everything.
  251.         ImmutableList<Dependency> projectDependencies = dependencyBuilder.mapProject(project,
  252.                 ScopeLimitingFilter.computeDependencyScope(ScopeLimitingFilter.COMPILE_PLUS_RUNTIME));

  253.         Map<String, Dependency> idMap = projectDependencies.stream()
  254.                 .filter(dependency -> dependency.getArtifact() != null)
  255.                 .collect(ImmutableMap.toImmutableMap(InlineMojo::getId, Functions.identity()));

  256.         BiConsumer<InlineDependency, Dependency> dependencyConsumer = (inlineDependency, dependency) -> {
  257.             LOG.debug("%s matches %s for inlining.", inlineDependency, dependency);
  258.             dependencyMapBuilder.put(inlineDependency, dependency);
  259.         };

  260.         ImmutableSet.Builder<Dependency> directExcludes = ImmutableSet.builder();

  261.         // first find all the direct dependencies. Add anything that is not hit to the additional exclude list

  262.         ImmutableSortedSet.Builder<String> directLogBuilder = ImmutableSortedSet.naturalOrder();

  263.         directDependencies.stream()
  264.                 // remove anything that does not match the filter set.
  265.                 // optionals also need to be matched by the inline dependency below
  266.                 .filter(createFilterSet(true))
  267.                 .forEach(dependency -> {
  268.                     Optional<InlineDependency> inlineDependency = findInlineDependencyMatch(dependency);
  269.                     if (inlineDependency.isPresent()) {
  270.                         dependencyConsumer.accept(inlineDependency.get(), dependency);
  271.                         directLogBuilder.add(dependency.toString());
  272.                     } else {
  273.                         directExcludes.add(dependency);
  274.                     }
  275.                 });

  276.         ImmutableSortedSet<String> directLog = directLogBuilder.build();

  277.         if (!quiet) {
  278.             LOG.info("Inlined dependencies");
  279.             LOG.info("====================");

  280.             for (String dependency : directLog) {
  281.                 LOG.info("    %s", dependency);
  282.             }
  283.             LOG.info("");
  284.         }

  285.         Set<ArtifactIdentifier> excludes = directExcludes.build().stream()
  286.                 .map(ArtifactIdentifier::new)
  287.                 .collect(Collectors.toUnmodifiableSet());

  288.         this.excludes = ImmutableList.copyOf(Iterables.concat(this.excludes, excludes));

  289.         LOG.debug("Excludes after creating includes: %s", this.excludes);

  290.         var directDependencyMap = dependencyMapBuilder.build().asMap();

  291.         ImmutableSortedSet.Builder<String> transitiveLogBuilder = ImmutableSortedSet.naturalOrder();

  292.         for (var dependencyEntry : directDependencyMap.entrySet()) {
  293.             InlineDependency inlineDependency = dependencyEntry.getKey();
  294.             for (Dependency projectDependency : dependencyEntry.getValue()) {

  295.                 Consumer<Dependency> consumer;
  296.                 if (inlineDependency.isInlineTransitive()) {
  297.                     // transitive deps are added to the jar
  298.                     consumer = dependency -> {
  299.                         Optional<InlineDependency> explicitMatch = findInlineDependencyMatch(dependency);

  300.                         // If the dependency is not a runtime dependency, it is included in the inline jar
  301.                         // Runtime dependencies are only included if they are explicitly listed as an
  302.                         // included dependency. Otherwise, they are added as a runtime dep to the inline jar.
  303.                         if (!JavaScopes.RUNTIME.equals(dependency.getScope()) || explicitMatch.isPresent()) {
  304.                             dependencyConsumer.accept(inlineDependency, dependency);
  305.                             transitiveLogBuilder.add(dependency.toString());
  306.                         } else {
  307.                             pomBuilder.add(dependency);
  308.                         }
  309.                     };
  310.                 } else {
  311.                     // non-transitive deps need to be written into the POM.
  312.                     consumer = pomBuilder::add;
  313.                 }

  314.                 dependencyBuilder.mapDependency(projectDependency, ScopeLimitingFilter.computeTransitiveScope(projectDependency.getScope()))
  315.                         .stream()
  316.                         // replace deps in the transitive set with deps in the root set if present (will
  317.                         // override the scope here with the root scope)
  318.                         .map(dependency -> idMap.getOrDefault(getId(dependency), dependency))
  319.                         // remove system and provided dependencies, keep optionals if allowed
  320.                         .filter(createFilterSet(inlineDependency.isInlineOptionals()))
  321.                         // make sure that the inline dependency actually pulls the dep in.
  322.                         .filter(this::isDependencyIncluded)
  323.                         .forEach(consumer);
  324.             }
  325.         }

  326.         if (!quiet) {
  327.             LOG.info("");
  328.             LOG.info("Transitive dependencies");
  329.             LOG.info("=======================");
  330.             for (String dependency : Sets.difference(transitiveLogBuilder.build(), directLog)) {
  331.                 LOG.info("    %s", dependency);
  332.             }
  333.             LOG.info("");
  334.         }
  335.     }

  336.     private Optional<InlineDependency> findInlineDependencyMatch(Dependency dependency) {
  337.         for (InlineDependency inlineDependency : inlineDependencies) {
  338.             if (inlineDependency.matchDependency(dependency)) {
  339.                 return Optional.of(inlineDependency);
  340.             }
  341.         }
  342.         return Optional.empty();
  343.     }

  344.     private static String getId(Dependency dependency) {
  345.         Artifact artifact = dependency.getArtifact();
  346.         checkState(artifact != null, "Artifact for dependency %s is null!", dependency);

  347.         return Joiner.on(':').join(artifact.getGroupId(), artifact.getArtifactId(), artifact.getClassifier());
  348.     }

  349.     private Predicate<Dependency> createFilterSet(boolean includeOptional) {

  350.         // filter system scope dependencies. Those are never inlined.
  351.         Predicate<Dependency> predicate = EXCLUDE_SYSTEM_SCOPE;
  352.         predicate = predicate.and(EXCLUDE_PROVIDED_SCOPE);

  353.         if (!includeOptional) {
  354.             predicate = predicate.and(Predicate.not(Dependency::isOptional));
  355.         }
  356.         return predicate;
  357.     }

  358.     public boolean isDependencyIncluded(Dependency dependency) {

  359.         boolean included = this.includes.stream()
  360.                 .map(artifactIdentifier -> artifactIdentifier.matchDependency(dependency))
  361.                 .findFirst()
  362.                 .orElse(this.includes.isEmpty());

  363.         boolean excluded = this.excludes.stream()
  364.                 .map(artifactIdentifier -> artifactIdentifier.matchDependency(dependency))
  365.                 .findFirst()
  366.                 .orElse(false);

  367.         return included && !excluded;
  368.     }


  369.     private void rewriteJarFile(ImmutableSetMultimap<InlineDependency, Dependency> dependencies) throws TransformerException, IOException {
  370.         File outputJar = (this.outputJarFile != null) ? outputJarFile : inlinedArtifactFileWithClassifier();

  371.         doJarTransformation(outputJar, dependencies);

  372.         if (this.outputJarFile == null) {
  373.             if (this.inlinedArtifactAttached) {
  374.                 LOG.info("Attaching inlined artifact.");
  375.                 projectHelper.attachArtifact(project, project.getArtifact().getType(), inlinedClassifierName, outputJar);
  376.             } else {
  377.                 LOG.info("Replacing original artifact with inlined artifact.");
  378.                 File originalArtifact = project.getArtifact().getFile();

  379.                 if (originalArtifact != null) {
  380.                     File backupFile = new File(originalArtifact.getParentFile(), "original-" + originalArtifact.getName());
  381.                     Files.move(originalArtifact.toPath(), backupFile.toPath(), ATOMIC_MOVE, REPLACE_EXISTING);
  382.                     Files.move(outputJar.toPath(), originalArtifact.toPath(), ATOMIC_MOVE, REPLACE_EXISTING);
  383.                 }
  384.             }
  385.         }
  386.     }

  387.     private void rewritePomFile(Set<Dependency> dependenciesToAdd, Set<Dependency> dependenciesToRemove) throws IOException, XMLStreamException, JDOMException {
  388.         String pomContents;

  389.         try (BufferedReader reader = Files.newBufferedReader(project.getFile().toPath(), StandardCharsets.UTF_8)) {
  390.             pomContents = CharStreams.toString(reader);
  391.         }

  392.         PomUtil pomUtil = new PomUtil(pomContents);
  393.         dependenciesToRemove.forEach(pomUtil::removeDependency);
  394.         dependenciesToAdd.forEach(pomUtil::addDependency);

  395.         // some rewriters (maven flatten plugin) rewrites the new pom name as a hidden file.
  396.         String pomName = this.pomFile.getName();
  397.         pomName = "new-" + (pomName.startsWith(".") ? pomName.substring(1) : pomName);

  398.         File newPomFile = this.outputPomFile != null ? outputPomFile : new File(this.outputDirectory, pomName);
  399.         try (BufferedWriter writer = Files.newBufferedWriter(newPomFile.toPath(), StandardCharsets.UTF_8)) {
  400.             pomUtil.writePom(writer);
  401.         }

  402.         if (this.replacePomFile) {
  403.             project.setPomFile(newPomFile);
  404.         }
  405.     }

  406.     private void doJarTransformation(File outputJar, ImmutableSetMultimap<InlineDependency, Dependency> dependencies) throws TransformerException, IOException {

  407.         try (JarOutputStream jarOutputStream = new JarOutputStream(Files.newOutputStream(outputJar.toPath()))) {
  408.             Consumer<ClassPathResource> jarConsumer = getJarWriter(jarOutputStream);
  409.             JarTransformer transformer = new JarTransformer(jarConsumer, true, ImmutableSet.copyOf(additionalProcessors));

  410.             // Build the class path
  411.             ClassPath classPath = new ClassPath(project.getBasedir());
  412.             // maintain the manifest file for the main artifact
  413.             var artifact = project.getArtifact();
  414.             classPath.addFile(artifact.getFile(), artifact.getGroupId(), artifact.getArtifactId(), ClassPathTag.ROOT_JAR);

  415.             dependencies.forEach(
  416.                     (inlineDependency, dependency) -> {
  417.                         var dependencyArtifact = dependency.getArtifact();
  418.                         checkState(dependencyArtifact.getFile() != null, "Could not locate artifact file for %s", dependencyArtifact);
  419.                         classPath.addFile(dependencyArtifact.getFile(), prefix, dependencyArtifact.getGroupId(), dependencyArtifact.getArtifactId(),
  420.                                 hideClasses);
  421.                     });

  422.             transformer.transform(classPath);
  423.         }
  424.     }

  425.     private Consumer<ClassPathResource> getJarWriter(JarOutputStream jarOutputStream) {
  426.         return classPathResource -> {
  427.             try {
  428.                 String name = classPathResource.getName();
  429.                 LOG.debug(format("Writing '%s' to jar", name));
  430.                 JarEntry outputEntry = new JarEntry(name);
  431.                 outputEntry.setTime(classPathResource.getLastModifiedTime());
  432.                 outputEntry.setCompressedSize(-1);
  433.                 jarOutputStream.putNextEntry(outputEntry);
  434.                 jarOutputStream.write(classPathResource.getContent());
  435.             } catch (IOException e) {
  436.                 throw new UncheckedIOException(e);
  437.             }
  438.         };
  439.     }

  440.     private File inlinedArtifactFileWithClassifier() {
  441.         final var artifact = project.getArtifact();
  442.         String inlineName = String.format("%s-%s-%s.%s",
  443.                 project.getArtifactId(),
  444.                 artifact.getVersion(),
  445.                 this.inlinedClassifierName,
  446.                 artifact.getArtifactHandler().getExtension());

  447.         return new File(this.outputDirectory, inlineName);
  448.     }
  449. }