View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.lifecycle;
20  
21  import java.io.File;
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.Collections;
25  import java.util.List;
26  
27  import org.apache.maven.AbstractCoreMavenComponentTestCase;
28  import org.apache.maven.exception.ExceptionHandler;
29  import org.apache.maven.execution.MavenSession;
30  import org.apache.maven.execution.MojoExecutionEvent;
31  import org.apache.maven.execution.MojoExecutionListener;
32  import org.apache.maven.execution.ProjectDependencyGraph;
33  import org.apache.maven.execution.ProjectExecutionEvent;
34  import org.apache.maven.execution.ProjectExecutionListener;
35  import org.apache.maven.lifecycle.internal.DefaultLifecycleTaskSegmentCalculator;
36  import org.apache.maven.lifecycle.internal.ExecutionPlanItem;
37  import org.apache.maven.lifecycle.internal.LifecycleExecutionPlanCalculator;
38  import org.apache.maven.lifecycle.internal.LifecycleTask;
39  import org.apache.maven.lifecycle.internal.LifecycleTaskSegmentCalculator;
40  import org.apache.maven.lifecycle.internal.MojoDescriptorCreator;
41  import org.apache.maven.lifecycle.internal.TaskSegment;
42  import org.apache.maven.model.Plugin;
43  import org.apache.maven.plugin.MojoExecution;
44  import org.apache.maven.plugin.MojoExecutionException;
45  import org.apache.maven.plugin.MojoNotFoundException;
46  import org.apache.maven.plugin.descriptor.MojoDescriptor;
47  import org.apache.maven.project.MavenProject;
48  import org.codehaus.plexus.component.annotations.Requirement;
49  import org.codehaus.plexus.util.xml.Xpp3Dom;
50  
51  public class LifecycleExecutorTest extends AbstractCoreMavenComponentTestCase {
52      @Requirement
53      private DefaultLifecycleExecutor lifecycleExecutor;
54  
55      @Requirement
56      private DefaultLifecycleTaskSegmentCalculator lifeCycleTaskSegmentCalculator;
57  
58      @Requirement
59      private LifecycleExecutionPlanCalculator lifeCycleExecutionPlanCalculator;
60  
61      @Requirement
62      private MojoDescriptorCreator mojoDescriptorCreator;
63  
64      protected void setUp() throws Exception {
65          super.setUp();
66          lifecycleExecutor = (DefaultLifecycleExecutor) lookup(LifecycleExecutor.class);
67          lifeCycleTaskSegmentCalculator =
68                  (DefaultLifecycleTaskSegmentCalculator) lookup(LifecycleTaskSegmentCalculator.class);
69          lifeCycleExecutionPlanCalculator = lookup(LifecycleExecutionPlanCalculator.class);
70          mojoDescriptorCreator = lookup(MojoDescriptorCreator.class);
71          lookup(ExceptionHandler.class);
72      }
73  
74      @Override
75      protected void tearDown() throws Exception {
76          lifecycleExecutor = null;
77          super.tearDown();
78      }
79  
80      protected String getProjectsDirectory() {
81          return "src/test/projects/lifecycle-executor";
82      }
83  
84      // -----------------------------------------------------------------------------------------------
85      // Tests which exercise the lifecycle executor when it is dealing with default lifecycle phases.
86      // -----------------------------------------------------------------------------------------------
87  
88      public void testCalculationOfBuildPlanWithIndividualTaskWherePluginIsSpecifiedInThePom() throws Exception {
89          // We are doing something like "mvn resources:resources" where no version is specified but this
90          // project we are working on has the version specified in the POM so the version should come from there.
91          File pom = getProject("project-basic");
92          MavenSession session = createMavenSession(pom);
93          assertEquals("project-basic", session.getCurrentProject().getArtifactId());
94          assertEquals("1.0", session.getCurrentProject().getVersion());
95          List<MojoExecution> executionPlan = getExecutions(calculateExecutionPlan(session, "resources:resources"));
96          assertEquals(1, executionPlan.size());
97          MojoExecution mojoExecution = executionPlan.get(0);
98          assertNotNull(mojoExecution);
99          assertEquals(
100                 "org.apache.maven.plugins",
101                 mojoExecution.getMojoDescriptor().getPluginDescriptor().getGroupId());
102         assertEquals(
103                 "maven-resources-plugin",
104                 mojoExecution.getMojoDescriptor().getPluginDescriptor().getArtifactId());
105         assertEquals(
106                 "0.1", mojoExecution.getMojoDescriptor().getPluginDescriptor().getVersion());
107     }
108 
109     public void testCalculationOfBuildPlanWithIndividualTaskOfTheCleanLifecycle() throws Exception {
110         // We are doing something like "mvn clean:clean" where no version is specified but this
111         // project we are working on has the version specified in the POM so the version should come from there.
112         File pom = getProject("project-basic");
113         MavenSession session = createMavenSession(pom);
114         assertEquals("project-basic", session.getCurrentProject().getArtifactId());
115         assertEquals("1.0", session.getCurrentProject().getVersion());
116         List<MojoExecution> executionPlan = getExecutions(calculateExecutionPlan(session, "clean"));
117         assertEquals(1, executionPlan.size());
118         MojoExecution mojoExecution = executionPlan.get(0);
119         assertNotNull(mojoExecution);
120         assertEquals(
121                 "org.apache.maven.plugins",
122                 mojoExecution.getMojoDescriptor().getPluginDescriptor().getGroupId());
123         assertEquals(
124                 "maven-clean-plugin",
125                 mojoExecution.getMojoDescriptor().getPluginDescriptor().getArtifactId());
126         assertEquals(
127                 "0.1", mojoExecution.getMojoDescriptor().getPluginDescriptor().getVersion());
128     }
129 
130     public void testCalculationOfBuildPlanWithIndividualTaskOfTheCleanCleanGoal() throws Exception {
131         // We are doing something like "mvn clean:clean" where no version is specified but this
132         // project we are working on has the version specified in the POM so the version should come from there.
133         File pom = getProject("project-basic");
134         MavenSession session = createMavenSession(pom);
135         assertEquals("project-basic", session.getCurrentProject().getArtifactId());
136         assertEquals("1.0", session.getCurrentProject().getVersion());
137         List<MojoExecution> executionPlan = getExecutions(calculateExecutionPlan(session, "clean:clean"));
138         assertEquals(1, executionPlan.size());
139         MojoExecution mojoExecution = executionPlan.get(0);
140         assertNotNull(mojoExecution);
141         assertEquals(
142                 "org.apache.maven.plugins",
143                 mojoExecution.getMojoDescriptor().getPluginDescriptor().getGroupId());
144         assertEquals(
145                 "maven-clean-plugin",
146                 mojoExecution.getMojoDescriptor().getPluginDescriptor().getArtifactId());
147         assertEquals(
148                 "0.1", mojoExecution.getMojoDescriptor().getPluginDescriptor().getVersion());
149     }
150 
151     List<MojoExecution> getExecutions(MavenExecutionPlan mavenExecutionPlan) {
152         List<MojoExecution> result = new ArrayList<>();
153         for (ExecutionPlanItem executionPlanItem : mavenExecutionPlan) {
154             result.add(executionPlanItem.getMojoExecution());
155         }
156         return result;
157     }
158 
159     // We need to take in multiple lifecycles
160     public void testCalculationOfBuildPlanTasksOfTheCleanLifecycleAndTheInstallLifecycle() throws Exception {
161         File pom = getProject("project-with-additional-lifecycle-elements");
162         MavenSession session = createMavenSession(pom);
163         assertEquals(
164                 "project-with-additional-lifecycle-elements",
165                 session.getCurrentProject().getArtifactId());
166         assertEquals("1.0", session.getCurrentProject().getVersion());
167         List<MojoExecution> executionPlan = getExecutions(calculateExecutionPlan(session, "clean", "install"));
168 
169         // [01] clean:clean
170         // [02] resources:resources
171         // [03] compiler:compile
172         // [04] it:generate-metadata
173         // [05] resources:testResources
174         // [06] compiler:testCompile
175         // [07] it:generate-test-metadata
176         // [08] surefire:test
177         // [09] jar:jar
178         // [10] install:install
179         //
180         assertEquals(10, executionPlan.size());
181 
182         assertEquals("clean:clean", executionPlan.get(0).getMojoDescriptor().getFullGoalName());
183         assertEquals(
184                 "resources:resources", executionPlan.get(1).getMojoDescriptor().getFullGoalName());
185         assertEquals(
186                 "compiler:compile", executionPlan.get(2).getMojoDescriptor().getFullGoalName());
187         assertEquals(
188                 "it:generate-metadata", executionPlan.get(3).getMojoDescriptor().getFullGoalName());
189         assertEquals(
190                 "resources:testResources",
191                 executionPlan.get(4).getMojoDescriptor().getFullGoalName());
192         assertEquals(
193                 "compiler:testCompile", executionPlan.get(5).getMojoDescriptor().getFullGoalName());
194         assertEquals(
195                 "it:generate-test-metadata",
196                 executionPlan.get(6).getMojoDescriptor().getFullGoalName());
197         assertEquals("surefire:test", executionPlan.get(7).getMojoDescriptor().getFullGoalName());
198         assertEquals("jar:jar", executionPlan.get(8).getMojoDescriptor().getFullGoalName());
199         assertEquals("install:install", executionPlan.get(9).getMojoDescriptor().getFullGoalName());
200     }
201 
202     // We need to take in multiple lifecycles
203     public void testCalculationOfBuildPlanWithMultipleExecutionsOfModello() throws Exception {
204         File pom = getProject("project-with-multiple-executions");
205         MavenSession session = createMavenSession(pom);
206         assertEquals(
207                 "project-with-multiple-executions", session.getCurrentProject().getArtifactId());
208         assertEquals("1.0.1", session.getCurrentProject().getVersion());
209 
210         MavenExecutionPlan plan = calculateExecutionPlan(session, "clean", "install");
211 
212         List<MojoExecution> executions = getExecutions(plan);
213 
214         // [01] clean:clean
215         // [02] modello:xpp3-writer
216         // [03] modello:java
217         // [04] modello:xpp3-reader
218         // [05] modello:xpp3-writer
219         // [06] modello:java
220         // [07] modello:xpp3-reader
221         // [08] plugin:descriptor
222         // [09] resources:resources
223         // [10] compiler:compile
224         // [11] resources:testResources
225         // [12] compiler:testCompile
226         // [13] surefire:test
227         // [14] jar:jar
228         // [15] plugin:addPluginArtifactMetadata
229         // [16] install:install
230         //
231 
232         assertEquals(16, executions.size());
233 
234         assertEquals("clean:clean", executions.get(0).getMojoDescriptor().getFullGoalName());
235         assertEquals("it:xpp3-writer", executions.get(1).getMojoDescriptor().getFullGoalName());
236         assertEquals("it:java", executions.get(2).getMojoDescriptor().getFullGoalName());
237         assertEquals("it:xpp3-reader", executions.get(3).getMojoDescriptor().getFullGoalName());
238         assertEquals("it:xpp3-writer", executions.get(4).getMojoDescriptor().getFullGoalName());
239         assertEquals("it:java", executions.get(5).getMojoDescriptor().getFullGoalName());
240         assertEquals("it:xpp3-reader", executions.get(6).getMojoDescriptor().getFullGoalName());
241         assertEquals(
242                 "resources:resources", executions.get(7).getMojoDescriptor().getFullGoalName());
243         assertEquals("compiler:compile", executions.get(8).getMojoDescriptor().getFullGoalName());
244         assertEquals("plugin:descriptor", executions.get(9).getMojoDescriptor().getFullGoalName());
245         assertEquals(
246                 "resources:testResources",
247                 executions.get(10).getMojoDescriptor().getFullGoalName());
248         assertEquals(
249                 "compiler:testCompile", executions.get(11).getMojoDescriptor().getFullGoalName());
250         assertEquals("surefire:test", executions.get(12).getMojoDescriptor().getFullGoalName());
251         assertEquals("jar:jar", executions.get(13).getMojoDescriptor().getFullGoalName());
252         assertEquals(
253                 "plugin:addPluginArtifactMetadata",
254                 executions.get(14).getMojoDescriptor().getFullGoalName());
255         assertEquals("install:install", executions.get(15).getMojoDescriptor().getFullGoalName());
256 
257         assertEquals(
258                 "src/main/mdo/remote-resources.mdo",
259                 new MojoExecutionXPathContainer(executions.get(1)).getValue("configuration/models[1]/model"));
260         assertEquals(
261                 "src/main/mdo/supplemental-model.mdo",
262                 new MojoExecutionXPathContainer(executions.get(4)).getValue("configuration/models[1]/model"));
263     }
264 
265     public void testLifecycleQueryingUsingADefaultLifecyclePhase() throws Exception {
266         File pom = getProject("project-with-additional-lifecycle-elements");
267         MavenSession session = createMavenSession(pom);
268         assertEquals(
269                 "project-with-additional-lifecycle-elements",
270                 session.getCurrentProject().getArtifactId());
271         assertEquals("1.0", session.getCurrentProject().getVersion());
272         List<MojoExecution> executionPlan = getExecutions(calculateExecutionPlan(session, "package"));
273 
274         // [01] resources:resources
275         // [02] compiler:compile
276         // [03] it:generate-metadata
277         // [04] resources:testResources
278         // [05] compiler:testCompile
279         // [06] plexus-component-metadata:generate-test-metadata
280         // [07] surefire:test
281         // [08] jar:jar
282         //
283         assertEquals(8, executionPlan.size());
284 
285         assertEquals(
286                 "resources:resources", executionPlan.get(0).getMojoDescriptor().getFullGoalName());
287         assertEquals(
288                 "compiler:compile", executionPlan.get(1).getMojoDescriptor().getFullGoalName());
289         assertEquals(
290                 "it:generate-metadata", executionPlan.get(2).getMojoDescriptor().getFullGoalName());
291         assertEquals(
292                 "resources:testResources",
293                 executionPlan.get(3).getMojoDescriptor().getFullGoalName());
294         assertEquals(
295                 "compiler:testCompile", executionPlan.get(4).getMojoDescriptor().getFullGoalName());
296         assertEquals(
297                 "it:generate-test-metadata",
298                 executionPlan.get(5).getMojoDescriptor().getFullGoalName());
299         assertEquals("surefire:test", executionPlan.get(6).getMojoDescriptor().getFullGoalName());
300         assertEquals("jar:jar", executionPlan.get(7).getMojoDescriptor().getFullGoalName());
301     }
302 
303     public void testLifecyclePluginsRetrievalForDefaultLifecycle() throws Exception {
304         List<Plugin> plugins = new ArrayList<>(lifecycleExecutor.getPluginsBoundByDefaultToAllLifecycles("jar"));
305 
306         assertEquals(8, plugins.size());
307     }
308 
309     public void testPluginConfigurationCreation() throws Exception {
310         File pom = getProject("project-with-additional-lifecycle-elements");
311         MavenSession session = createMavenSession(pom);
312         MojoDescriptor mojoDescriptor = mojoDescriptorCreator.getMojoDescriptor(
313                 "org.apache.maven.its.plugins:maven-it-plugin:0.1:java", session, session.getCurrentProject());
314         Xpp3Dom dom = MojoDescriptorCreator.convert(mojoDescriptor);
315         System.out.println(dom);
316     }
317 
318     MavenExecutionPlan calculateExecutionPlan(MavenSession session, String... tasks) throws Exception {
319         List<TaskSegment> taskSegments =
320                 lifeCycleTaskSegmentCalculator.calculateTaskSegments(session, Arrays.asList(tasks));
321 
322         TaskSegment mergedSegment = new TaskSegment(false);
323 
324         for (TaskSegment taskSegment : taskSegments) {
325             mergedSegment.getTasks().addAll(taskSegment.getTasks());
326         }
327 
328         return lifeCycleExecutionPlanCalculator.calculateExecutionPlan(
329                 session, session.getCurrentProject(), mergedSegment.getTasks());
330     }
331 
332     public void testInvalidGoalName() throws Exception {
333         File pom = getProject("project-basic");
334         MavenSession session = createMavenSession(pom);
335         try {
336             getExecutions(calculateExecutionPlan(session, "resources:"));
337             fail("expected a MojoNotFoundException");
338         } catch (MojoNotFoundException e) {
339             assertEquals("", e.getGoal());
340         }
341 
342         try {
343             getExecutions(calculateExecutionPlan(
344                     session, "org.apache.maven.plugins:maven-resources-plugin:0.1:resources:toomany"));
345             fail("expected a MojoNotFoundException");
346         } catch (MojoNotFoundException e) {
347             assertEquals("resources:toomany", e.getGoal());
348         }
349     }
350 
351     public void testPluginPrefixRetrieval() throws Exception {
352         File pom = getProject("project-basic");
353         MavenSession session = createMavenSession(pom);
354         Plugin plugin = mojoDescriptorCreator.findPluginForPrefix("resources", session);
355         assertEquals("org.apache.maven.plugins", plugin.getGroupId());
356         assertEquals("maven-resources-plugin", plugin.getArtifactId());
357     }
358 
359     // Prefixes
360 
361     public void testFindingPluginPrefixforCleanClean() throws Exception {
362         File pom = getProject("project-basic");
363         MavenSession session = createMavenSession(pom);
364         Plugin plugin = mojoDescriptorCreator.findPluginForPrefix("clean", session);
365         assertNotNull(plugin);
366     }
367 
368     public void testSetupMojoExecution() throws Exception {
369         File pom = getProject("mojo-configuration");
370 
371         MavenSession session = createMavenSession(pom);
372 
373         LifecycleTask task = new LifecycleTask("generate-sources");
374         MavenExecutionPlan executionPlan = lifeCycleExecutionPlanCalculator.calculateExecutionPlan(
375                 session, session.getCurrentProject(), Arrays.asList((Object) task), false);
376 
377         MojoExecution execution = executionPlan.getMojoExecutions().get(0);
378         assertEquals(execution.toString(), "maven-it-plugin", execution.getArtifactId());
379         assertNull(execution.getConfiguration());
380 
381         lifeCycleExecutionPlanCalculator.setupMojoExecution(session, session.getCurrentProject(), execution);
382         assertNotNull(execution.getConfiguration());
383         assertEquals("1.0", execution.getConfiguration().getChild("version").getAttribute("default-value"));
384     }
385 
386     public void testExecutionListeners() throws Exception {
387         final File pom = getProject("project-basic");
388         final MavenSession session = createMavenSession(pom);
389         session.setProjectDependencyGraph(new ProjectDependencyGraph() {
390             public List<MavenProject> getUpstreamProjects(MavenProject project, boolean transitive) {
391                 return Collections.emptyList();
392             }
393 
394             public List<MavenProject> getAllProjects() {
395                 return session.getAllProjects();
396             }
397 
398             public List<MavenProject> getSortedProjects() {
399                 return Collections.singletonList(session.getCurrentProject());
400             }
401 
402             public List<MavenProject> getDownstreamProjects(MavenProject project, boolean transitive) {
403                 return Collections.emptyList();
404             }
405 
406             public java.util.List<MavenProject> getAllSortedProjects() {
407                 return Collections.emptyList();
408             }
409         });
410 
411         final List<String> log = new ArrayList<>();
412 
413         MojoExecutionListener mojoListener = new MojoExecutionListener() {
414             public void beforeMojoExecution(MojoExecutionEvent event) throws MojoExecutionException {
415                 assertNotNull(event.getSession());
416                 assertNotNull(event.getProject());
417                 assertNotNull(event.getExecution());
418                 assertNotNull(event.getMojo());
419                 assertNull(event.getCause());
420 
421                 log.add("beforeMojoExecution " + event.getProject().getArtifactId() + ":"
422                         + event.getExecution().getExecutionId());
423             }
424 
425             public void afterMojoExecutionSuccess(MojoExecutionEvent event) throws MojoExecutionException {
426                 assertNotNull(event.getSession());
427                 assertNotNull(event.getProject());
428                 assertNotNull(event.getExecution());
429                 assertNotNull(event.getMojo());
430                 assertNull(event.getCause());
431 
432                 log.add("afterMojoExecutionSuccess " + event.getProject().getArtifactId() + ":"
433                         + event.getExecution().getExecutionId());
434             }
435 
436             public void afterExecutionFailure(MojoExecutionEvent event) {
437                 assertNotNull(event.getSession());
438                 assertNotNull(event.getProject());
439                 assertNotNull(event.getExecution());
440                 assertNotNull(event.getMojo());
441                 assertNotNull(event.getCause());
442 
443                 log.add("afterExecutionFailure " + event.getProject().getArtifactId() + ":"
444                         + event.getExecution().getExecutionId());
445             }
446         };
447         ProjectExecutionListener projectListener = new ProjectExecutionListener() {
448             public void beforeProjectExecution(ProjectExecutionEvent event) throws LifecycleExecutionException {
449                 assertNotNull(event.getSession());
450                 assertNotNull(event.getProject());
451                 assertNull(event.getExecutionPlan());
452                 assertNull(event.getCause());
453 
454                 log.add("beforeProjectExecution " + event.getProject().getArtifactId());
455             }
456 
457             public void beforeProjectLifecycleExecution(ProjectExecutionEvent event)
458                     throws LifecycleExecutionException {
459                 assertNotNull(event.getSession());
460                 assertNotNull(event.getProject());
461                 assertNotNull(event.getExecutionPlan());
462                 assertNull(event.getCause());
463 
464                 log.add("beforeProjectLifecycleExecution " + event.getProject().getArtifactId());
465             }
466 
467             public void afterProjectExecutionSuccess(ProjectExecutionEvent event) throws LifecycleExecutionException {
468                 assertNotNull(event.getSession());
469                 assertNotNull(event.getProject());
470                 assertNotNull(event.getExecutionPlan());
471                 assertNull(event.getCause());
472 
473                 log.add("afterProjectExecutionSuccess " + event.getProject().getArtifactId());
474             }
475 
476             public void afterProjectExecutionFailure(ProjectExecutionEvent event) {
477                 assertNotNull(event.getSession());
478                 assertNotNull(event.getProject());
479                 assertNull(event.getExecutionPlan());
480                 assertNotNull(event.getCause());
481 
482                 log.add("afterProjectExecutionFailure " + event.getProject().getArtifactId());
483             }
484         };
485         lookup(DelegatingProjectExecutionListener.class).addProjectExecutionListener(projectListener);
486         lookup(DelegatingMojoExecutionListener.class).addMojoExecutionListener(mojoListener);
487 
488         try {
489             lifecycleExecutor.execute(session);
490         } finally {
491             lookup(DelegatingProjectExecutionListener.class).removeProjectExecutionListener(projectListener);
492             lookup(DelegatingMojoExecutionListener.class).removeMojoExecutionListener(mojoListener);
493         }
494 
495         List<String> expectedLog = Arrays.asList(
496                 "beforeProjectExecution project-basic", //
497                 "beforeProjectLifecycleExecution project-basic", //
498                 "beforeMojoExecution project-basic:default-resources", //
499                 "afterMojoExecutionSuccess project-basic:default-resources", //
500                 "beforeMojoExecution project-basic:default-compile", //
501                 "afterMojoExecutionSuccess project-basic:default-compile", //
502                 "beforeMojoExecution project-basic:default-testResources", //
503                 "afterMojoExecutionSuccess project-basic:default-testResources", //
504                 "beforeMojoExecution project-basic:default-testCompile", //
505                 "afterMojoExecutionSuccess project-basic:default-testCompile", //
506                 "beforeMojoExecution project-basic:default-test", //
507                 "afterMojoExecutionSuccess project-basic:default-test", //
508                 "beforeMojoExecution project-basic:default-jar", //
509                 "afterMojoExecutionSuccess project-basic:default-jar", //
510                 "afterProjectExecutionSuccess project-basic" //
511                 );
512 
513         assertEventLog(expectedLog, log);
514     }
515 
516     private static void assertEventLog(List<String> expectedList, List<String> actualList) {
517         assertEquals(toString(expectedList), toString(actualList));
518     }
519 
520     private static String toString(List<String> lines) {
521         StringBuilder sb = new StringBuilder();
522         for (String line : lines) {
523             sb.append(line).append('\n');
524         }
525         return sb.toString();
526     }
527 }