001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.maven.tools.plugin.generator; 020 021import java.io.File; 022import java.io.IOException; 023import java.io.OutputStreamWriter; 024import java.io.Writer; 025import java.net.URI; 026import java.util.LinkedHashMap; 027import java.util.LinkedHashSet; 028import java.util.List; 029import java.util.Map; 030import java.util.Set; 031 032import org.apache.maven.plugin.descriptor.MojoDescriptor; 033import org.apache.maven.plugin.descriptor.Parameter; 034import org.apache.maven.plugin.descriptor.PluginDescriptor; 035import org.apache.maven.plugin.descriptor.Requirement; 036import org.apache.maven.project.MavenProject; 037import org.apache.maven.tools.plugin.ExtendedMojoDescriptor; 038import org.apache.maven.tools.plugin.ExtendedPluginDescriptor; 039import org.apache.maven.tools.plugin.PluginToolsRequest; 040import org.apache.maven.tools.plugin.javadoc.JavadocLinkGenerator; 041import org.apache.maven.tools.plugin.util.PluginUtils; 042import org.codehaus.plexus.util.StringUtils; 043import org.codehaus.plexus.util.io.CachingOutputStream; 044import org.codehaus.plexus.util.xml.PrettyPrintXMLWriter; 045import org.codehaus.plexus.util.xml.XMLWriter; 046import org.slf4j.Logger; 047import org.slf4j.LoggerFactory; 048 049import static java.nio.charset.StandardCharsets.UTF_8; 050 051/** 052 * Serializes 053 * <ol> 054 * <li>a standard <a href="/ref/current/maven-plugin-api/plugin.html">Maven Plugin Descriptor XML file</a></li> 055 * <li>a descriptor containing a limited set of elements for {@link PluginHelpGenerator}</li> 056 * <li>an enhanced descriptor containing HTML values for some elements (instead of plain text as for the other two) 057 * for {@code org.apache.maven.plugin.plugin.report.GoalRenderer}</li> 058 * </ol> 059 * from a given in-memory descriptor. The in-memory descriptor acting as source is supposed to contain XHTML values 060 * for description elements. 061 * 062 */ 063public class PluginDescriptorFilesGenerator implements Generator { 064 private static final Logger LOG = LoggerFactory.getLogger(PluginDescriptorFilesGenerator.class); 065 066 /** 067 * The type of the plugin descriptor file 068 */ 069 enum DescriptorType { 070 STANDARD, 071 LIMITED_FOR_HELP_MOJO, 072 XHTML 073 } 074 075 @Override 076 public void execute(File destinationDirectory, PluginToolsRequest request) throws GeneratorException { 077 try { 078 // write standard plugin.xml descriptor 079 File f = new File(destinationDirectory, "plugin.xml"); 080 writeDescriptor(f, request, DescriptorType.STANDARD); 081 082 // write plugin-help.xml help-descriptor (containing only a limited set of attributes) 083 MavenProject mavenProject = request.getProject(); 084 f = new File(destinationDirectory, PluginHelpGenerator.getPluginHelpPath(mavenProject)); 085 writeDescriptor(f, request, DescriptorType.LIMITED_FOR_HELP_MOJO); 086 087 // write enhanced plugin-enhanced.xml descriptor (containing some XHTML values) 088 f = getEnhancedDescriptorFilePath(mavenProject); 089 writeDescriptor(f, request, DescriptorType.XHTML); 090 } catch (IOException e) { 091 throw new GeneratorException(e.getMessage(), e); 092 } 093 } 094 095 public static File getEnhancedDescriptorFilePath(MavenProject project) { 096 return new File(project.getBuild().getDirectory(), "plugin-enhanced.xml"); 097 } 098 099 private String getVersion() { 100 Package p = this.getClass().getPackage(); 101 String version = (p == null) ? null : p.getSpecificationVersion(); 102 return (version == null) ? "SNAPSHOT" : version; 103 } 104 105 public void writeDescriptor(File destinationFile, PluginToolsRequest request, DescriptorType type) 106 throws IOException { 107 PluginDescriptor pluginDescriptor = request.getPluginDescriptor(); 108 109 if (!destinationFile.getParentFile().exists()) { 110 destinationFile.getParentFile().mkdirs(); 111 } 112 113 try (Writer writer = new OutputStreamWriter(new CachingOutputStream(destinationFile), UTF_8)) { 114 XMLWriter w = new PrettyPrintXMLWriter(writer, UTF_8.name(), null); 115 116 final String additionalInfo; 117 switch (type) { 118 case LIMITED_FOR_HELP_MOJO: 119 additionalInfo = " (for help mojo with limited elements)"; 120 break; 121 case XHTML: 122 additionalInfo = " (enhanced XHTML version (used for plugin:report))"; 123 break; 124 default: 125 additionalInfo = ""; 126 break; 127 } 128 w.writeMarkup("\n<!-- Generated by maven-plugin-tools " + getVersion() + additionalInfo + "-->\n\n"); 129 130 w.startElement("plugin"); 131 132 GeneratorUtils.element(w, "name", pluginDescriptor.getName()); 133 134 GeneratorUtils.element(w, "description", pluginDescriptor.getDescription()); 135 136 GeneratorUtils.element(w, "groupId", pluginDescriptor.getGroupId()); 137 138 GeneratorUtils.element(w, "artifactId", pluginDescriptor.getArtifactId()); 139 140 GeneratorUtils.element(w, "version", pluginDescriptor.getVersion()); 141 142 GeneratorUtils.element(w, "goalPrefix", pluginDescriptor.getGoalPrefix()); 143 144 if (type != DescriptorType.LIMITED_FOR_HELP_MOJO) { 145 GeneratorUtils.element(w, "isolatedRealm", String.valueOf(pluginDescriptor.isIsolatedRealm())); 146 147 GeneratorUtils.element( 148 w, "inheritedByDefault", String.valueOf(pluginDescriptor.isInheritedByDefault())); 149 150 if (pluginDescriptor instanceof ExtendedPluginDescriptor) { 151 ExtendedPluginDescriptor extPluginDescriptor = (ExtendedPluginDescriptor) pluginDescriptor; 152 if (StringUtils.isNotBlank(extPluginDescriptor.getRequiredJavaVersion())) { 153 GeneratorUtils.element(w, "requiredJavaVersion", extPluginDescriptor.getRequiredJavaVersion()); 154 } 155 } 156 if (StringUtils.isNotBlank(pluginDescriptor.getRequiredMavenVersion())) { 157 GeneratorUtils.element(w, "requiredMavenVersion", pluginDescriptor.getRequiredMavenVersion()); 158 } 159 } 160 161 w.startElement("mojos"); 162 163 final JavadocLinkGenerator javadocLinkGenerator; 164 if (request.getInternalJavadocBaseUrl() != null 165 || (request.getExternalJavadocBaseUrls() != null 166 && !request.getExternalJavadocBaseUrls().isEmpty())) { 167 javadocLinkGenerator = new JavadocLinkGenerator( 168 request.getInternalJavadocBaseUrl(), 169 request.getInternalJavadocVersion(), 170 request.getExternalJavadocBaseUrls(), 171 request.getSettings()); 172 } else { 173 javadocLinkGenerator = null; 174 } 175 if (pluginDescriptor.getMojos() != null) { 176 List<MojoDescriptor> descriptors = pluginDescriptor.getMojos(); 177 178 PluginUtils.sortMojos(descriptors); 179 180 for (MojoDescriptor descriptor : descriptors) { 181 processMojoDescriptor(descriptor, w, type, javadocLinkGenerator); 182 } 183 } 184 185 w.endElement(); 186 187 if (type != DescriptorType.LIMITED_FOR_HELP_MOJO) { 188 GeneratorUtils.writeDependencies(w, pluginDescriptor); 189 } 190 191 w.endElement(); 192 193 writer.flush(); 194 } 195 } 196 197 /** 198 * 199 * @param type 200 * @param containsXhtmlValue 201 * @param text 202 * @return the normalized text value (i.e. potentially converted to XHTML) 203 */ 204 private static String getTextValue(DescriptorType type, boolean containsXhtmlValue, String text) { 205 final String xhtmlText; 206 if (!containsXhtmlValue) // text comes from legacy extractor 207 { 208 xhtmlText = GeneratorUtils.makeHtmlValid(text); 209 } else { 210 xhtmlText = text; 211 } 212 if (type != DescriptorType.XHTML) { 213 return new HtmlToPlainTextConverter().convert(text); 214 } else { 215 return xhtmlText; 216 } 217 } 218 219 @SuppressWarnings("deprecation") 220 protected void processMojoDescriptor( 221 MojoDescriptor mojoDescriptor, 222 XMLWriter w, 223 DescriptorType type, 224 JavadocLinkGenerator javadocLinkGenerator) { 225 boolean containsXhtmlTextValues = mojoDescriptor instanceof ExtendedMojoDescriptor 226 && ((ExtendedMojoDescriptor) mojoDescriptor).containsXhtmlTextValues(); 227 228 w.startElement("mojo"); 229 230 // ---------------------------------------------------------------------- 231 // 232 // ---------------------------------------------------------------------- 233 234 w.startElement("goal"); 235 w.writeText(mojoDescriptor.getGoal()); 236 w.endElement(); 237 238 // ---------------------------------------------------------------------- 239 // 240 // ---------------------------------------------------------------------- 241 242 String description = mojoDescriptor.getDescription(); 243 244 if (description != null && !description.isEmpty()) { 245 w.startElement("description"); 246 w.writeText(getTextValue(type, containsXhtmlTextValues, mojoDescriptor.getDescription())); 247 w.endElement(); 248 } 249 250 // ---------------------------------------------------------------------- 251 // 252 // ---------------------------------------------------------------------- 253 254 if (StringUtils.isNotEmpty(mojoDescriptor.isDependencyResolutionRequired())) { 255 GeneratorUtils.element(w, "requiresDependencyResolution", mojoDescriptor.isDependencyResolutionRequired()); 256 } 257 258 // ---------------------------------------------------------------------- 259 // 260 // ---------------------------------------------------------------------- 261 262 GeneratorUtils.element(w, "requiresDirectInvocation", String.valueOf(mojoDescriptor.isDirectInvocationOnly())); 263 264 // ---------------------------------------------------------------------- 265 // 266 // ---------------------------------------------------------------------- 267 268 GeneratorUtils.element(w, "requiresProject", String.valueOf(mojoDescriptor.isProjectRequired())); 269 270 // ---------------------------------------------------------------------- 271 // 272 // ---------------------------------------------------------------------- 273 274 GeneratorUtils.element(w, "requiresReports", String.valueOf(mojoDescriptor.isRequiresReports())); 275 276 // ---------------------------------------------------------------------- 277 // 278 // ---------------------------------------------------------------------- 279 280 GeneratorUtils.element(w, "aggregator", String.valueOf(mojoDescriptor.isAggregator())); 281 282 // ---------------------------------------------------------------------- 283 // 284 // ---------------------------------------------------------------------- 285 286 GeneratorUtils.element(w, "requiresOnline", String.valueOf(mojoDescriptor.isOnlineRequired())); 287 288 // ---------------------------------------------------------------------- 289 // 290 // ---------------------------------------------------------------------- 291 292 GeneratorUtils.element(w, "inheritedByDefault", String.valueOf(mojoDescriptor.isInheritedByDefault())); 293 294 // ---------------------------------------------------------------------- 295 // 296 // ---------------------------------------------------------------------- 297 298 if (StringUtils.isNotEmpty(mojoDescriptor.getPhase())) { 299 GeneratorUtils.element(w, "phase", mojoDescriptor.getPhase()); 300 } 301 302 // ---------------------------------------------------------------------- 303 // 304 // ---------------------------------------------------------------------- 305 306 if (StringUtils.isNotEmpty(mojoDescriptor.getExecutePhase())) { 307 GeneratorUtils.element(w, "executePhase", mojoDescriptor.getExecutePhase()); 308 } 309 310 if (StringUtils.isNotEmpty(mojoDescriptor.getExecuteGoal())) { 311 GeneratorUtils.element(w, "executeGoal", mojoDescriptor.getExecuteGoal()); 312 } 313 314 if (StringUtils.isNotEmpty(mojoDescriptor.getExecuteLifecycle())) { 315 GeneratorUtils.element(w, "executeLifecycle", mojoDescriptor.getExecuteLifecycle()); 316 } 317 318 // ---------------------------------------------------------------------- 319 // 320 // ---------------------------------------------------------------------- 321 322 w.startElement("implementation"); 323 w.writeText(mojoDescriptor.getImplementation()); 324 w.endElement(); 325 326 // ---------------------------------------------------------------------- 327 // 328 // ---------------------------------------------------------------------- 329 330 w.startElement("language"); 331 w.writeText(mojoDescriptor.getLanguage()); 332 w.endElement(); 333 334 // ---------------------------------------------------------------------- 335 // 336 // ---------------------------------------------------------------------- 337 338 if (StringUtils.isNotEmpty(mojoDescriptor.getComponentConfigurator())) { 339 w.startElement("configurator"); 340 w.writeText(mojoDescriptor.getComponentConfigurator()); 341 w.endElement(); 342 } 343 344 // ---------------------------------------------------------------------- 345 // 346 // ---------------------------------------------------------------------- 347 348 if (StringUtils.isNotEmpty(mojoDescriptor.getComponentComposer())) { 349 w.startElement("composer"); 350 w.writeText(mojoDescriptor.getComponentComposer()); 351 w.endElement(); 352 } 353 354 // ---------------------------------------------------------------------- 355 // 356 // ---------------------------------------------------------------------- 357 358 w.startElement("instantiationStrategy"); 359 w.writeText(mojoDescriptor.getInstantiationStrategy()); 360 w.endElement(); 361 362 // ---------------------------------------------------------------------- 363 // Strategy for handling repeated reference to mojo in 364 // the calculated (decorated, resolved) execution stack 365 // ---------------------------------------------------------------------- 366 w.startElement("executionStrategy"); 367 w.writeText(mojoDescriptor.getExecutionStrategy()); 368 w.endElement(); 369 370 // ---------------------------------------------------------------------- 371 // 372 // ---------------------------------------------------------------------- 373 374 if (mojoDescriptor.getSince() != null) { 375 w.startElement("since"); 376 377 if (StringUtils.isEmpty(mojoDescriptor.getSince())) { 378 w.writeText("No version given"); 379 } else { 380 w.writeText(mojoDescriptor.getSince()); 381 } 382 383 w.endElement(); 384 } 385 386 // ---------------------------------------------------------------------- 387 // 388 // ---------------------------------------------------------------------- 389 390 if (mojoDescriptor.getDeprecated() != null) { 391 w.startElement("deprecated"); 392 393 if (StringUtils.isEmpty(mojoDescriptor.getDeprecated())) { 394 w.writeText("No reason given"); 395 } else { 396 w.writeText(getTextValue(type, containsXhtmlTextValues, mojoDescriptor.getDeprecated())); 397 } 398 399 w.endElement(); 400 } 401 402 // ---------------------------------------------------------------------- 403 // Extended (3.0) descriptor 404 // ---------------------------------------------------------------------- 405 406 if (mojoDescriptor instanceof ExtendedMojoDescriptor) { 407 ExtendedMojoDescriptor extendedMojoDescriptor = (ExtendedMojoDescriptor) mojoDescriptor; 408 if (extendedMojoDescriptor.getDependencyCollectionRequired() != null) { 409 GeneratorUtils.element( 410 w, "requiresDependencyCollection", extendedMojoDescriptor.getDependencyCollectionRequired()); 411 } 412 413 GeneratorUtils.element(w, "threadSafe", String.valueOf(extendedMojoDescriptor.isThreadSafe())); 414 415 boolean v4Api = extendedMojoDescriptor.isV4Api(); 416 if (v4Api) { 417 GeneratorUtils.element(w, "v4Api", String.valueOf(v4Api)); 418 } 419 } 420 421 // ---------------------------------------------------------------------- 422 // Parameters 423 // ---------------------------------------------------------------------- 424 425 List<Parameter> parameters = mojoDescriptor.getParameters(); 426 427 w.startElement("parameters"); 428 429 Map<String, Requirement> requirements = new LinkedHashMap<>(); 430 431 Set<Parameter> configuration = new LinkedHashSet<>(); 432 433 if (parameters != null) { 434 if (type == DescriptorType.LIMITED_FOR_HELP_MOJO) { 435 PluginUtils.sortMojoParameters(parameters); 436 } 437 438 for (Parameter parameter : parameters) { 439 String expression = getExpression(parameter); 440 441 if ((expression != null && !expression.isEmpty()) && expression.startsWith("${component.")) { 442 // treat it as a component...a requirement, in other words. 443 444 // remove "component." plus expression delimiters 445 String role = expression.substring("${component.".length(), expression.length() - 1); 446 447 String roleHint = null; 448 449 int posRoleHintSeparator = role.indexOf('#'); 450 if (posRoleHintSeparator > 0) { 451 roleHint = role.substring(posRoleHintSeparator + 1); 452 453 role = role.substring(0, posRoleHintSeparator); 454 } 455 456 // TODO: remove deprecated expression 457 requirements.put(parameter.getName(), new Requirement(role, roleHint)); 458 } else if (parameter.getRequirement() != null) { 459 requirements.put(parameter.getName(), parameter.getRequirement()); 460 } 461 // don't show readonly parameters in help 462 else if (type != DescriptorType.LIMITED_FOR_HELP_MOJO || parameter.isEditable()) { 463 // treat it as a normal parameter. 464 465 w.startElement("parameter"); 466 467 GeneratorUtils.element(w, "name", parameter.getName()); 468 469 if (parameter.getAlias() != null) { 470 GeneratorUtils.element(w, "alias", parameter.getAlias()); 471 } 472 473 writeParameterType(w, type, javadocLinkGenerator, parameter, mojoDescriptor.getGoal()); 474 475 if (parameter.getSince() != null) { 476 w.startElement("since"); 477 478 if (StringUtils.isEmpty(parameter.getSince())) { 479 w.writeText("No version given"); 480 } else { 481 w.writeText(parameter.getSince()); 482 } 483 484 w.endElement(); 485 } 486 487 if (parameter.getDeprecated() != null) { 488 if (StringUtils.isEmpty(parameter.getDeprecated())) { 489 GeneratorUtils.element(w, "deprecated", "No reason given"); 490 } else { 491 GeneratorUtils.element( 492 w, 493 "deprecated", 494 getTextValue(type, containsXhtmlTextValues, parameter.getDeprecated())); 495 } 496 } 497 498 if (parameter.getImplementation() != null) { 499 GeneratorUtils.element(w, "implementation", parameter.getImplementation()); 500 } 501 502 GeneratorUtils.element(w, "required", Boolean.toString(parameter.isRequired())); 503 504 GeneratorUtils.element(w, "editable", Boolean.toString(parameter.isEditable())); 505 506 GeneratorUtils.element( 507 w, "description", getTextValue(type, containsXhtmlTextValues, parameter.getDescription())); 508 509 if (StringUtils.isNotEmpty(parameter.getDefaultValue()) 510 || StringUtils.isNotEmpty(parameter.getExpression())) { 511 configuration.add(parameter); 512 } 513 514 w.endElement(); 515 } 516 } 517 } 518 519 w.endElement(); 520 521 // ---------------------------------------------------------------------- 522 // Configuration 523 // ---------------------------------------------------------------------- 524 525 if (!configuration.isEmpty()) { 526 w.startElement("configuration"); 527 528 for (Parameter parameter : configuration) { 529 if (type == DescriptorType.LIMITED_FOR_HELP_MOJO && !parameter.isEditable()) { 530 // don't show readonly parameters in help 531 continue; 532 } 533 534 w.startElement(parameter.getName()); 535 536 // strip type by parameter type (generics) information 537 String parameterType = StringUtils.chomp(parameter.getType(), "<"); 538 if (parameterType != null && !parameterType.isEmpty()) { 539 w.addAttribute("implementation", parameterType); 540 } 541 542 if (parameter.getDefaultValue() != null) { 543 w.addAttribute("default-value", parameter.getDefaultValue()); 544 } 545 546 if (StringUtils.isNotEmpty(parameter.getExpression())) { 547 w.writeText(parameter.getExpression()); 548 } 549 550 w.endElement(); 551 } 552 553 w.endElement(); 554 } 555 556 // ---------------------------------------------------------------------- 557 // Requirements 558 // ---------------------------------------------------------------------- 559 560 if (!requirements.isEmpty() && type != DescriptorType.LIMITED_FOR_HELP_MOJO) { 561 w.startElement("requirements"); 562 563 for (Map.Entry<String, Requirement> entry : requirements.entrySet()) { 564 String key = entry.getKey(); 565 Requirement requirement = entry.getValue(); 566 567 w.startElement("requirement"); 568 569 GeneratorUtils.element(w, "role", requirement.getRole()); 570 571 if (StringUtils.isNotEmpty(requirement.getRoleHint())) { 572 GeneratorUtils.element(w, "role-hint", requirement.getRoleHint()); 573 } 574 575 GeneratorUtils.element(w, "field-name", key); 576 577 w.endElement(); 578 } 579 580 w.endElement(); 581 } 582 583 w.endElement(); 584 } 585 586 /** 587 * Writes parameter type information and potentially also the related javadoc URL. 588 * @param w 589 * @param type 590 * @param javadocLinkGenerator 591 * @param parameter 592 * @param goal 593 */ 594 protected void writeParameterType( 595 XMLWriter w, 596 DescriptorType type, 597 JavadocLinkGenerator javadocLinkGenerator, 598 Parameter parameter, 599 String goal) { 600 String parameterType = parameter.getType(); 601 602 if (type == DescriptorType.STANDARD) { 603 // strip type by parameter type (generics) information for standard plugin descriptor 604 parameterType = StringUtils.chomp(parameterType, "<"); 605 } 606 GeneratorUtils.element(w, "type", parameterType); 607 608 if (type == DescriptorType.XHTML && javadocLinkGenerator != null) { 609 // skip primitives which never has javadoc 610 if (parameter.getType().indexOf('.') == -1) { 611 LOG.debug("Javadoc URLs are not available for primitive types like {}", parameter.getType()); 612 } else { 613 try { 614 URI javadocUrl = getJavadocUrlForType(javadocLinkGenerator, parameterType); 615 GeneratorUtils.element(w, "typeJavadocUrl", javadocUrl.toString()); 616 } catch (IllegalArgumentException e) { 617 LOG.warn( 618 "Could not get javadoc URL for type {} of parameter {} from goal {}: {}", 619 parameter.getType(), 620 parameter.getName(), 621 goal, 622 e.getMessage()); 623 } 624 } 625 } 626 } 627 628 private static String extractBinaryNameForJavadoc(String type) { 629 final String binaryName; 630 int startOfParameterType = type.indexOf("<"); 631 if (startOfParameterType != -1) { 632 // parse parameter type 633 String mainType = type.substring(0, startOfParameterType); 634 635 // some heuristics here 636 String[] parameterTypes = type.substring(startOfParameterType + 1, type.lastIndexOf(">")) 637 .split(",\\s*"); 638 switch (parameterTypes.length) { 639 case 1: // if only one parameter type, assume collection, first parameter type is most interesting 640 binaryName = extractBinaryNameForJavadoc(parameterTypes[0]); 641 break; 642 case 2: // if two parameter types assume map, second parameter type is most interesting 643 binaryName = extractBinaryNameForJavadoc(parameterTypes[1]); 644 break; 645 default: 646 // all other cases link to main type 647 binaryName = mainType; 648 } 649 } else { 650 binaryName = type; 651 } 652 return binaryName; 653 } 654 655 static URI getJavadocUrlForType(JavadocLinkGenerator javadocLinkGenerator, String type) { 656 return javadocLinkGenerator.createLink(extractBinaryNameForJavadoc(type)); 657 } 658 659 /** 660 * Get the expression value, eventually surrounding it with <code>${ }</code>. 661 * 662 * @param parameter the parameter 663 * @return the expression value 664 */ 665 private String getExpression(Parameter parameter) { 666 String expression = parameter.getExpression(); 667 if (StringUtils.isNotBlank(expression) && !expression.contains("${")) { 668 expression = "${" + expression.trim() + "}"; 669 parameter.setExpression(expression); 670 } 671 return expression; 672 } 673}