001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2017-2019 microBean. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 014 * implied. See the License for the specific language governing 015 * permissions and limitations under the License. 016 */ 017package org.microbean.maven.cdi; 018 019import java.io.IOException; 020import java.io.InputStream; 021import java.io.File; 022 023import java.lang.annotation.Annotation; 024 025import java.util.ArrayList; 026import java.util.Collection; 027import java.util.Collections; 028import java.util.List; 029import java.util.Map; 030import java.util.Map.Entry; 031import java.util.Objects; 032import java.util.Set; 033 034import javax.enterprise.context.ApplicationScoped; 035 036import javax.enterprise.inject.Alternative; 037 038import javax.inject.Inject; 039 040import org.codehaus.plexus.interpolation.InterpolationException; 041import org.codehaus.plexus.interpolation.Interpolator; 042import org.codehaus.plexus.interpolation.RegexBasedInterpolator; 043import org.codehaus.plexus.interpolation.PropertiesBasedValueSource; 044import org.codehaus.plexus.interpolation.EnvarBasedValueSource; 045 046import org.codehaus.plexus.util.xml.Xpp3Dom; 047 048import org.apache.maven.building.FileSource; 049import org.apache.maven.building.Source; 050 051import org.apache.maven.settings.Server; 052import org.apache.maven.settings.Settings; 053import org.apache.maven.settings.TrackableBase; 054 055import org.apache.maven.settings.building.DefaultSettingsBuilder; // for javadoc only 056import org.apache.maven.settings.building.DefaultSettingsProblem; 057import org.apache.maven.settings.building.SettingsBuilder; 058import org.apache.maven.settings.building.SettingsBuildingException; 059import org.apache.maven.settings.building.SettingsBuildingRequest; 060import org.apache.maven.settings.building.SettingsBuildingResult; 061import org.apache.maven.settings.building.SettingsProblem; 062import org.apache.maven.settings.building.SettingsProblemCollector; 063 064import org.apache.maven.settings.io.SettingsParseException; 065 066import org.apache.maven.settings.merge.MavenSettingsMerger; 067 068import org.apache.maven.settings.validation.SettingsValidator; 069import org.apache.maven.settings.validation.DefaultSettingsValidator; 070 071import org.yaml.snakeyaml.TypeDescription; 072import org.yaml.snakeyaml.Yaml; 073 074import org.yaml.snakeyaml.constructor.Construct; 075import org.yaml.snakeyaml.constructor.Constructor; 076import org.yaml.snakeyaml.constructor.SafeConstructor; 077import org.yaml.snakeyaml.constructor.SafeConstructor.ConstructYamlOmap; 078 079import org.yaml.snakeyaml.error.Mark; 080import org.yaml.snakeyaml.error.MarkedYAMLException; 081import org.yaml.snakeyaml.error.YAMLException; 082 083import org.yaml.snakeyaml.introspector.BeanAccess; 084import org.yaml.snakeyaml.introspector.Property; 085import org.yaml.snakeyaml.introspector.PropertyUtils; 086 087import org.yaml.snakeyaml.nodes.Node; 088import org.yaml.snakeyaml.nodes.MappingNode; 089import org.yaml.snakeyaml.nodes.SequenceNode; 090import org.yaml.snakeyaml.nodes.ScalarNode; 091import org.yaml.snakeyaml.nodes.Tag; 092 093import org.yaml.snakeyaml.reader.ReaderException; 094 095/** 096 * A {@link SettingsBuilder} implementation that behaves like a {@link 097 * DefaultSettingsBuilder} implementation but without needlessly 098 * requiring round-trip serialization and deserialization of the 099 * underlying settings, and that reads YAML files instead of XML 100 * files. 101 * 102 * @author <a href="https://about.me/lairdnelson" 103 * target="_parent">Laird Nelson</a> 104 * 105 * @see #build(SettingsBuildingRequest) 106 * 107 * @see DefaultSettingsBuilder 108 */ 109@Alternative 110@ApplicationScoped 111public class YamlSettingsBuilder implements SettingsBuilder { 112 113 114 /* 115 * Instance fields. 116 */ 117 118 119 private final SettingsValidator settingsValidator; 120 121 private final MavenSettingsMerger merger; 122 123 124 /* 125 * Constructors. 126 */ 127 128 129 /** 130 * Creates a new {@link YamlSettingsBuilder} with a {@link 131 * DefaultSettingsValidator} and a {@link MavenSettingsMerger}. 132 * 133 * @see #YamlSettingsBuilder(SettingsValidator, MavenSettingsMerger) 134 */ 135 public YamlSettingsBuilder() { 136 this(new DefaultSettingsValidator(), new MavenSettingsMerger()); 137 } 138 139 /** 140 * Creates a new {@link YamlSettingsBuilder}. 141 * 142 * @param settingsValidator a {@link SettingsValidator} 143 * implementation that will validate the {@link Settings} object 144 * once it has been successfully deserialized; if {@code null} then 145 * a {@link DefaultSettingsValidator} implementation will be used 146 * instead 147 * 148 * @param merger a {@link MavenSettingsMerger} that will be used to 149 * merge global and user-specific {@link Settings} objects; if 150 * {@code null}, then a new {@link MavenSettingsMerger} will be used 151 * instead 152 */ 153 @Inject 154 public YamlSettingsBuilder(final SettingsValidator settingsValidator, 155 final MavenSettingsMerger merger) { 156 super(); 157 if (settingsValidator == null) { 158 this.settingsValidator = new DefaultSettingsValidator(); 159 } else { 160 this.settingsValidator = settingsValidator; 161 } 162 if (merger == null) { 163 this.merger = new MavenSettingsMerger(); 164 } else { 165 this.merger = merger; 166 } 167 } 168 169 170 /* 171 * Instance methods. 172 */ 173 174 175 /** 176 * Deserializes a {@link Settings} from a stream-based source 177 * represented by the supplied {@link SettingsBuildingRequest} and 178 * returns it wrapped by a {@link SettingsBuildingResult} in much 179 * the same manner as the {@link 180 * DefaultSettingsBuilder#build(SettingsBuildingRequest)} method, 181 * but using YAML instead of XML, and performing interpolation in a 182 * more efficient manner. 183 * 184 * <p>This method never returns {@code null}.</p> 185 * 186 * <p>Overrides of this method must not return {@code null}.</p> 187 * 188 * @param request the {@link SettingsBuildingRequest} representing 189 * the settings location; must not be {@code null} 190 * 191 * @return a non-{@code null} {@link SettingsBuildingResult} 192 * 193 * @see #read(Source, boolean, Interpolator, 194 * SettingsProblemCollector) 195 * 196 * @exception NullPointerException if {@code request} is {@code null} 197 * 198 * @exception SettingsBuildingException if an error occurred, but 199 * also see the return value of the {@link 200 * SettingsBuildingResult#getProblems()} method 201 */ 202 @Override 203 public SettingsBuildingResult build(final SettingsBuildingRequest request) throws SettingsBuildingException { 204 Objects.requireNonNull(request); 205 final List<SettingsProblem> problems = new ArrayList<>(); 206 207 Source globalSettingsSource = request.getGlobalSettingsSource(); 208 if (globalSettingsSource == null) { 209 final File file = request.getGlobalSettingsFile(); 210 if (file != null && file.exists()) { 211 globalSettingsSource = new FileSource(file); 212 } 213 } 214 final Settings globalSettings; 215 if (globalSettingsSource == null) { 216 globalSettings = null; 217 } else { 218 globalSettings = this.readSettings(globalSettingsSource, request, problems); 219 } 220 221 Source userSettingsSource = request.getUserSettingsSource(); 222 if (userSettingsSource == null) { 223 final File file = request.getUserSettingsFile(); 224 if (file != null && file.exists()) { 225 userSettingsSource = new FileSource(file); 226 } 227 } 228 Settings settings = null; 229 if (userSettingsSource != null) { 230 settings = this.readSettings(userSettingsSource, request, problems); 231 } 232 233 if (settings == null) { 234 if (globalSettings != null) { 235 settings = globalSettings; 236 } 237 } else if (globalSettings != null) { 238 this.merger.merge(settings, globalSettings, TrackableBase.GLOBAL_LEVEL); 239 } 240 241 if (hasErrors(problems)) { 242 throw new SettingsBuildingException(problems); 243 } 244 245 final SettingsBuildingResult returnValue = new DefaultSettingsBuildingResult(settings, problems); 246 return returnValue; 247 } 248 249 /** 250 * Given a {@link Source}, reads settings information from it and 251 * creates a new {@link Settings} object and returns it. 252 * 253 * <p>This method may return {@code null}.</p> 254 * 255 * <p>Overrides of this method are permitted to return {@code null}.</p> 256 * 257 * @param source the {@link Source} representing the location of the 258 * settings; may be {@code null} in which case {@code null} will be 259 * returned 260 * 261 * @param strict whether or not strictness should be in effect 262 * 263 * @param interpolator an {@link Interpolator} for interpolating 264 * values within the settings information; may be {@code null} 265 * 266 * @param problemCollector a {@link SettingsProblemCollector} to 267 * accumulate error information; must not be {@code null} 268 * 269 * @return a {@link Settings}, or {@code null} 270 * 271 * @exception NullPointerException if {@code problemCollector} is 272 * {@code null} 273 * 274 * @exception IOException if an input or output error occurred 275 * 276 * @exception SettingsParseException if the settings information 277 * could not be parsed as a YAML 1.1 document 278 */ 279 public Settings read(final Source source, 280 final boolean strict, 281 final Interpolator interpolator, 282 final SettingsProblemCollector problemCollector) 283 throws IOException, SettingsParseException { 284 Objects.requireNonNull(problemCollector); 285 Settings returnValue = null; 286 if (source != null) { 287 try (final InputStream stream = source.getInputStream()) { 288 if (stream != null) { 289 final Constructor constructor = new InterpolatingConstructor(interpolator, problemCollector); 290 final Yaml yaml = new Yaml(constructor); 291 yaml.addTypeDescription(new TypeDescription(Server.class) { 292 293 @Override 294 public final boolean setupPropertyType(final String key, final Node valueNode) { 295 final boolean returnValue; 296 if ("configuration".equals(key) && valueNode != null) { 297 valueNode.setType(Xpp3Dom.class); 298 returnValue = true; 299 } else { 300 returnValue = super.setupPropertyType(key, valueNode); 301 } 302 return returnValue; 303 } 304 305 @Override 306 public final Object newInstance(final String propertyName, final Node node) { 307 final Object returnValue; 308 if ("configuration".equals(propertyName) && (node instanceof MappingNode || node instanceof SequenceNode)) { 309 Xpp3Dom dom = new Xpp3Dom("configuration"); 310 returnValue = dom; 311 final Object contents = new HackyConstructor().construct(node); 312 if (contents instanceof Map) { 313 handleConfigurationMap(interpolator, problemCollector, dom, (Map<?, ?>)contents); 314 } else if (contents instanceof Collection) { 315 handleConfigurationCollection(interpolator, problemCollector, dom, (Collection<?>)contents); 316 } else { 317 throw new MarkedYAMLException("after deserializing configuration", 318 node.getStartMark(), 319 "found invalid scalar configuration: " + contents, 320 node.getStartMark()) { 321 private static final long serialVersionUID = 1L; 322 }; 323 } 324 } else { 325 returnValue = super.newInstance(propertyName, node); 326 } 327 return returnValue; 328 } 329 }); 330 returnValue = yaml.loadAs(stream, Settings.class); 331 } 332 } catch (final MarkedYAMLException e) { 333 final Mark mark = e.getProblemMark(); 334 final int line; 335 final int column; 336 if (mark == null) { 337 line = -1; 338 column = -1; 339 } else { 340 line = mark.getLine() + 1; 341 column = mark.getColumn() + 1; 342 } 343 throw new SettingsParseException(e.getMessage(), line, column, e); 344 } catch (final ReaderException e) { 345 throw new SettingsParseException(e.getMessage(), -1, e.getPosition() + 1, e); 346 } catch (final YAMLException e) { 347 throw new SettingsParseException(e.getMessage(), -1, -1, e); 348 } 349 } 350 return returnValue; 351 } 352 353 private static final void handleConfigurationScalar(final Interpolator interpolator, 354 final SettingsProblemCollector problemCollector, 355 final Xpp3Dom element, 356 final Object scalarItem) { 357 Objects.requireNonNull(element); 358 Objects.requireNonNull(problemCollector); 359 if (scalarItem != null) { 360 assert !(scalarItem instanceof Collection); 361 assert !(scalarItem instanceof Map); 362 String value = scalarItem.toString(); 363 if (interpolator != null) { 364 try { 365 value = interpolator.interpolate(value, "settings"); 366 } catch (final InterpolationException interpolationException) { 367 problemCollector.add(SettingsProblem.Severity.ERROR, 368 "Failed to interpolate settings: " + 369 interpolationException.getMessage(), 370 -1, 371 -1, 372 interpolationException); 373 } 374 } 375 element.setValue(value); 376 } 377 } 378 379 private static final void handleConfigurationCollection(final Interpolator interpolator, 380 final SettingsProblemCollector problemCollector, 381 final Xpp3Dom rootElement, 382 final Collection<?> items) { 383 Objects.requireNonNull(rootElement); 384 if (items != null && !items.isEmpty()) { 385 for (final Object item : items) { 386 if (item instanceof Map) { 387 handleConfigurationMap(interpolator, problemCollector, rootElement, (Map<?, ?>)item); 388 } else if (item instanceof Collection) { 389 throw new YAMLException("Invalid configuration element (after deserialization): " + item); 390 } else { 391 handleConfigurationScalar(interpolator, problemCollector, rootElement, item); 392 } 393 } 394 } 395 } 396 397 private static final void handleConfigurationMap(final Interpolator interpolator, 398 final SettingsProblemCollector problemCollector, 399 final Xpp3Dom rootElement, 400 final Map<?, ?> items) { 401 Objects.requireNonNull(rootElement); 402 if (items != null && !items.isEmpty()) { 403 final Set<? extends Entry<?, ?>> entrySet = items.entrySet(); 404 if (entrySet != null && !entrySet.isEmpty()) { 405 for (final Entry<?, ?> entry : entrySet) { 406 if (entry != null) { 407 final Object key = entry.getKey(); 408 final Xpp3Dom element = new Xpp3Dom(String.valueOf(key)); 409 element.setParent(rootElement); 410 rootElement.addChild(element); 411 final Object value = entry.getValue(); 412 if (value != null) { 413 if (value instanceof Collection) { 414 handleConfigurationCollection(interpolator, problemCollector, element, (Collection<?>)value); 415 } else if (value instanceof Map) { 416 handleConfigurationMap(interpolator, problemCollector, element, (Map<?, ?>)value); // RECURSIVE 417 } else { 418 handleConfigurationScalar(interpolator, problemCollector, element, value); 419 } 420 } 421 } 422 } 423 } 424 } 425 } 426 427 private final Settings readSettings(final Source source, 428 final SettingsBuildingRequest request, 429 final Collection<SettingsProblem> problems) { 430 Objects.requireNonNull(problems); 431 Settings returnValue = null; 432 if (source != null) { 433 SettingsProblemCollector collector = 434 (severity, message, line, column, cause) -> 435 add(problems, severity, message, source.getLocation(), line, column, cause); 436 final Interpolator interpolator = new RegexBasedInterpolator(); 437 interpolator.addValueSource(new PropertiesBasedValueSource(request.getUserProperties())); 438 interpolator.addValueSource(new PropertiesBasedValueSource(request.getSystemProperties())); 439 try { 440 interpolator.addValueSource(new EnvarBasedValueSource()); 441 } catch (final IOException ioException) { 442 collector.add(SettingsProblem.Severity.WARNING, 443 "Failed to use environment variables for interpolation: " + 444 ioException.getMessage(), 445 -1, 446 -1, 447 ioException); 448 } 449 try { 450 try { 451 returnValue = this.read(source, true, interpolator, collector); 452 this.settingsValidator.validate(returnValue, collector); 453 } catch (final SettingsParseException strictParsingFailed) { 454 returnValue = this.read(source, false, interpolator, collector); 455 // Record the warning only if lenient reading succeeded. 456 collector.add(SettingsProblem.Severity.WARNING, 457 strictParsingFailed.getMessage(), 458 strictParsingFailed.getLineNumber(), 459 strictParsingFailed.getColumnNumber(), 460 strictParsingFailed); 461 this.settingsValidator.validate(returnValue, collector); 462 } 463 } catch (final SettingsParseException lenientParsingFailed) { 464 collector.add(SettingsProblem.Severity.FATAL, 465 "Non-parseable settings " + 466 source.getLocation() + 467 ": " + 468 lenientParsingFailed.getMessage(), 469 lenientParsingFailed.getLineNumber(), 470 lenientParsingFailed.getColumnNumber(), 471 lenientParsingFailed); 472 } catch (final IOException ioException) { 473 collector.add(SettingsProblem.Severity.FATAL, 474 "Non-readable settings " + 475 source.getLocation() + 476 ": " + 477 ioException.getMessage(), 478 -1, 479 -1, 480 ioException); 481 } 482 } 483 return returnValue; 484 } 485 486 487 /* 488 * Static methods. 489 */ 490 491 492 private static final boolean hasErrors(Collection<? extends SettingsProblem> problems) { 493 boolean returnValue = false; 494 if (problems != null && !problems.isEmpty()) { 495 for (final SettingsProblem problem : problems) { 496 if (problem != null && SettingsProblem.Severity.ERROR.compareTo(problem.getSeverity()) >= 0) { 497 returnValue = true; 498 break; 499 } 500 } 501 } 502 return returnValue; 503 } 504 505 private static final void add(final Collection<SettingsProblem> problems, 506 final SettingsProblem.Severity severity, 507 final String message, 508 final String source, 509 int line, 510 int column, 511 final Exception cause) { 512 Objects.requireNonNull(problems); 513 if (cause instanceof SettingsParseException && line <= 0 && column <= 0) { 514 final SettingsParseException e = (SettingsParseException)cause; 515 line = e.getLineNumber(); 516 column = e.getColumnNumber(); 517 } 518 problems.add(new DefaultSettingsProblem(message, severity, source, line, column, cause)); 519 } 520 521 522 /* 523 * Inner and nested calsses. 524 */ 525 526 527 private static final class DefaultSettingsBuildingResult implements SettingsBuildingResult { 528 529 private final Settings settings; 530 531 private final List<SettingsProblem> problems; 532 533 private DefaultSettingsBuildingResult(final Settings settings, final List<SettingsProblem> problems) { 534 super(); 535 if (settings == null) { 536 this.settings = new Settings(); 537 } else { 538 this.settings = settings; 539 } 540 if (problems == null) { 541 this.problems = Collections.emptyList(); 542 } else { 543 this.problems = Collections.unmodifiableList(problems); 544 } 545 } 546 547 @Override 548 public final Settings getEffectiveSettings() { 549 return this.settings; 550 } 551 552 @Override 553 public final List<SettingsProblem> getProblems() { 554 return this.problems; 555 } 556 557 } 558 559 private static final class InterpolatingConstructor extends Constructor { 560 561 private final Interpolator interpolator; 562 563 private final SettingsProblemCollector problemCollector; 564 565 private InterpolatingConstructor(final Interpolator interpolator, 566 final SettingsProblemCollector problemCollector) { 567 super(Settings.class); 568 this.interpolator = interpolator; 569 if (interpolator == null) { 570 this.problemCollector = problemCollector; 571 } else { 572 this.problemCollector = Objects.requireNonNull(problemCollector); 573 } 574 } 575 576 @Override 577 protected final String constructScalar(final ScalarNode node) { 578 String returnValue = super.constructScalar(node); 579 if (this.interpolator != null && returnValue != null) { 580 try { 581 returnValue = this.interpolator.interpolate(returnValue, "settings"); 582 } catch (final InterpolationException interpolationException) { 583 assert this.problemCollector != null; 584 this.problemCollector.add(SettingsProblem.Severity.ERROR, 585 "Failed to interpolate settings: " + 586 interpolationException.getMessage(), 587 -1, 588 -1, 589 interpolationException); 590 } 591 } 592 return returnValue; 593 } 594 595 } 596 597 private static final class HackyConstructor extends SafeConstructor { 598 599 private HackyConstructor() { 600 super(); 601 } 602 603 private final Object construct(final Node node) { 604 return this.yamlConstructors.get(node.getTag()).construct(node); 605 } 606 607 } 608 609}