001/* 002 * Copyright 2007-2021 The jdeb developers. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.vafer.jdeb.maven; 018 019import java.io.File; 020import java.io.FileInputStream; 021import java.io.FileNotFoundException; 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.Collections; 025import java.util.HashMap; 026import java.util.HashSet; 027import java.util.List; 028import java.util.Map; 029import java.util.Set; 030 031import org.apache.commons.compress.archivers.tar.TarArchiveEntry; 032import org.apache.commons.compress.archivers.tar.TarConstants; 033import org.apache.maven.artifact.Artifact; 034import org.apache.maven.execution.MavenSession; 035import org.apache.maven.plugin.AbstractMojo; 036import org.apache.maven.plugin.MojoExecutionException; 037import org.apache.maven.plugins.annotations.Component; 038import org.apache.maven.plugins.annotations.LifecyclePhase; 039import org.apache.maven.plugins.annotations.Mojo; 040import org.apache.maven.plugins.annotations.Parameter; 041import org.apache.maven.project.MavenProject; 042import org.apache.maven.project.MavenProjectHelper; 043import org.apache.maven.settings.Profile; 044import org.apache.maven.settings.Settings; 045import org.apache.tools.tar.TarEntry; 046import org.sonatype.plexus.components.sec.dispatcher.SecDispatcher; 047import org.sonatype.plexus.components.sec.dispatcher.SecDispatcherException; 048import org.vafer.jdeb.Console; 049import org.vafer.jdeb.DataConsumer; 050import org.vafer.jdeb.DataProducer; 051import org.vafer.jdeb.DebMaker; 052import org.vafer.jdeb.PackagingException; 053import org.vafer.jdeb.utils.MapVariableResolver; 054import org.vafer.jdeb.utils.OutputTimestampResolver; 055import org.vafer.jdeb.utils.SymlinkUtils; 056import org.vafer.jdeb.utils.Utils; 057import org.vafer.jdeb.utils.VariableResolver; 058 059import static org.vafer.jdeb.utils.Utils.isBlank; 060import static org.vafer.jdeb.utils.Utils.lookupIfEmpty; 061 062/** 063 * Creates Debian package 064 */ 065@Mojo(name = "jdeb", defaultPhase = LifecyclePhase.PACKAGE, threadSafe = true) 066public class DebMojo extends AbstractMojo { 067 068 @Component 069 private MavenProjectHelper projectHelper; 070 071 @Component(hint = "jdeb-sec") 072 private SecDispatcher secDispatcher; 073 074 /** 075 * Defines the name of deb package. 076 */ 077 @Parameter 078 private String name; 079 080 /** 081 * Defines the pattern of the name of final artifacts. Possible 082 * substitutions are [[baseDir]] [[buildDir]] [[artifactId]] [[version]] 083 * [[extension]] and [[groupId]]. 084 */ 085 @Parameter(defaultValue = "[[buildDir]]/[[artifactId]]_[[version]]_all.[[extension]]") 086 private String deb; 087 088 /** 089 * Explicitly defines the path to the control directory. At least the 090 * control file is mandatory. 091 */ 092 @Parameter(defaultValue = "[[baseDir]]/src/deb/control") 093 private String controlDir; 094 095 /** 096 * Explicitly define the file to read the changes from. 097 */ 098 @Parameter(defaultValue = "[[baseDir]]/CHANGES.txt") 099 private String changesIn; 100 101 /** 102 * Explicitly define the file where to write the changes to. 103 */ 104 @Parameter(defaultValue = "[[buildDir]]/[[artifactId]]_[[version]]_all.changes") 105 private String changesOut; 106 107 /** 108 * Explicitly define the file where to write the changes of the changes input to. 109 */ 110 @Parameter(defaultValue = "[[baseDir]]/CHANGES.txt") 111 private String changesSave; 112 113 /** 114 * The compression method used for the data file (none, gzip, bzip2 or xz) 115 */ 116 @Parameter(defaultValue = "gzip") 117 private String compression; 118 119 /** 120 * Boolean option whether to attach the artifact to the project 121 */ 122 @Parameter(defaultValue = "true") 123 private String attach; 124 125 /** 126 * The location where all package files will be installed. By default, all 127 * packages are installed in /opt (see the FHS here: 128 * http://www.pathname.com/ 129 * fhs/pub/fhs-2.3.html#OPTADDONAPPLICATIONSOFTWAREPACKAGES) 130 */ 131 @Parameter(defaultValue = "/opt/[[artifactId]]") 132 private String installDir; 133 134 /** 135 * The type of attached artifact 136 */ 137 @Parameter(defaultValue = "deb") 138 private String type; 139 140 /** 141 * The project base directory 142 */ 143 @Parameter(defaultValue = "${basedir}", required = true, readonly = true) 144 private File baseDir; 145 146 /** 147 * The Maven Session Object 148 */ 149 @Parameter( defaultValue = "${session}", readonly = true ) 150 private MavenSession session; 151 152 /** 153 * The Maven Project Object 154 */ 155 @Parameter( defaultValue = "${project}", readonly = true ) 156 private MavenProject project; 157 158 /** 159 * The build directory 160 */ 161 @Parameter(property = "project.build.directory", required = true, readonly = true) 162 private File buildDirectory; 163 164 /** 165 * The classifier of attached artifact 166 */ 167 @Parameter 168 private String classifier; 169 170 /** 171 * The digest algorithm to use. 172 * 173 * @see org.bouncycastle.bcpg.HashAlgorithmTags 174 */ 175 @Parameter(defaultValue = "SHA256") 176 private String digest; 177 178 /** 179 * "data" entries used to determine which files should be added to this deb. 180 * The "data" entries may specify a tarball (tar.gz, tar.bz2, tgz), a 181 * directory, or a normal file. An entry would look something like this in 182 * your pom.xml: 183 * 184 * 185 * <pre> 186 * <build> 187 * <plugins> 188 * <plugin> 189 * <artifactId>jdeb</artifactId> 190 * <groupId>org.vafer</groupId> 191 * ... 192 * <configuration> 193 * ... 194 * <dataSet> 195 * <data> 196 * <src>${project.basedir}/target/my_archive.tar.gz</src> 197 * <include>...</include> 198 * <exclude>...</exclude> 199 * <mapper> 200 * <type>perm</type> 201 * <strip>1</strip> 202 * <prefix>/somewhere/else</prefix> 203 * <user>santbj</user> 204 * <group>santbj</group> 205 * <mode>600</mode> 206 * </mapper> 207 * </data> 208 * <data> 209 * <src>${project.build.directory}/data</src> 210 * <include></include> 211 * <exclude>**/.svn</exclude> 212 * <mapper> 213 * <type>ls</type> 214 * <src>mapping.txt</src> 215 * </mapper> 216 * </data> 217 * <data> 218 * <type>link</type> 219 * <linkName>/a/path/on/the/target/fs</linkName> 220 * <linkTarget>/a/sym/link/to/the/scr/file</linkTarget> 221 * <symlink>true</symlink> 222 * </data> 223 * <data> 224 * <src>${project.basedir}/README.txt</src> 225 * </data> 226 * </dataSet> 227 * </configuration> 228 * </plugins> 229 * </build> 230 * </pre> 231 * 232 */ 233 @Parameter 234 private Data[] dataSet; 235 236// /** 237// * @deprecated 238// */ 239// @Parameter(defaultValue = "false") 240// private boolean timestamped; 241 242 /** 243 * When enabled SNAPSHOT inside the version gets replaced with current timestamp or 244 * if set a value of a environment variable. 245 */ 246 @Parameter(defaultValue = "false") 247 private boolean snapshotExpand; 248 249 /** 250 * Which environment variable to check for the SNAPSHOT value. 251 * If the variable is not set/empty it will default to use the timestamp. 252 */ 253 @Parameter(defaultValue = "SNAPSHOT") 254 private String snapshotEnv; 255 256 /** 257 * Template for replacing the SNAPSHOT value. A timestamp format can be provided in brackets. 258 * prefix[yyMMdd]suffix -> prefix151230suffix 259 */ 260 @Parameter 261 private String snapshotTemplate; 262 263 /** 264 * If verbose is true more build messages are logged. 265 */ 266 @Parameter(defaultValue = "false") 267 private boolean verbose; 268 269 /** 270 * Indicates if the execution should be disabled. If <code>true</code>, nothing will occur during execution. 271 * 272 * @since 1.1 273 */ 274 @Parameter(property = "jdeb.skip", defaultValue = "false") 275 private boolean skip; 276 277 @Parameter(property = "jdeb.skipPOMs", defaultValue = "true") 278 private boolean skipPOMs; 279 280 @Parameter(property = "jdeb.skipSubmodules", defaultValue = "false") 281 private boolean skipSubmodules; 282 283 /** 284 * @deprecated 285 */ 286 @Parameter(defaultValue = "true") 287 private boolean submodules; 288 289 290 /** 291 * If signPackage is true then a origin signature will be placed 292 * in the generated package. 293 */ 294 @Parameter(defaultValue = "false") 295 private boolean signPackage; 296 297 /** 298 * If signChanges is true then changes file will be signed. 299 */ 300 @Parameter(defaultValue = "false") 301 private boolean signChanges; 302 303 /** 304 * Defines which utility is used to verify the signed package 305 */ 306 @Parameter(defaultValue = "debsig-verify") 307 private String signMethod; 308 309 /** 310 * Defines the role to sign with 311 */ 312 @Parameter(defaultValue = "origin") 313 private String signRole; 314 315 /** 316 * The keyring to use for signing operations. 317 */ 318 @Parameter 319 private String keyring; 320 321 /** 322 * The key to use for signing operations. 323 */ 324 @Parameter 325 private String key; 326 327 /** 328 * The passphrase to use for signing operations. 329 */ 330 @Parameter 331 private String passphrase; 332 333 /** 334 * The prefix to use when reading signing variables 335 * from settings. 336 */ 337 @Parameter(defaultValue = "jdeb.") 338 private String signCfgPrefix; 339 340 /** 341 * The settings. 342 */ 343 @Parameter(defaultValue = "${settings}") 344 private Settings settings; 345 346 @Parameter(defaultValue = "") 347 private String propertyPrefix; 348 349 /** 350 * Sets the long file mode for the resulting tar file. Valid values are "gnu", "posix", "error" or "truncate" 351 * @see org.apache.commons.compress.archivers.tar.TarArchiveOutputStream#setLongFileMode(int) 352 */ 353 @Parameter(defaultValue = "gnu") 354 private String tarLongFileMode; 355 356 /** 357 * Sets the big number mode for the resulting tar file. Valid values are "gnu", "posix" or "error" 358 * @see org.apache.commons.compress.archivers.tar.TarArchiveOutputStream#setBigNumberMode(int) 359 */ 360 @Parameter(defaultValue = "gnu") 361 private String tarBigNumberMode; 362 363 /** 364 * Timestamp for reproducible output archive entries, either formatted as ISO 8601 365 * <code>yyyy-MM-dd'T'HH:mm:ssXXX</code> or as an int representing seconds since the epoch (like 366 * <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>). 367 * 368 * @since 1.9 369 */ 370 @Parameter(defaultValue = "${project.build.outputTimestamp}") 371 private String outputTimestamp; 372 373 /* end of parameters */ 374 375 private static final String KEY = "key"; 376 private static final String KEYRING = "keyring"; 377 private static final String PASSPHRASE = "passphrase"; 378 379 private String openReplaceToken = "[["; 380 private String closeReplaceToken = "]]"; 381 private Console console; 382 private Collection<DataProducer> dataProducers = new ArrayList<>(); 383 private Collection<DataProducer> conffileProducers = new ArrayList<>(); 384 385 public void setOpenReplaceToken( String openReplaceToken ) { 386 this.openReplaceToken = openReplaceToken; 387 } 388 389 public void setCloseReplaceToken( String closeReplaceToken ) { 390 this.closeReplaceToken = closeReplaceToken; 391 } 392 393 protected void setData( Data[] dataSet ) { 394 this.dataSet = dataSet; 395 dataProducers.clear(); 396 conffileProducers.clear(); 397 if (dataSet != null) { 398 Collections.addAll(dataProducers, dataSet); 399 400 for (Data item : dataSet) { 401 if (item.getConffile()) { 402 conffileProducers.add(item); 403 } 404 } 405 } 406 } 407 408 @SuppressWarnings("unchecked,rawtypes") 409 protected VariableResolver initializeVariableResolver( Map<String, String> variables ) { 410 variables.putAll((Map) getProject().getProperties()); 411 variables.putAll((Map) System.getProperties()); 412 variables.put("name", name != null ? name : getProject().getName()); 413 variables.put("artifactId", getProject().getArtifactId()); 414 variables.put("groupId", getProject().getGroupId()); 415 variables.put("version", getProjectVersion()); 416 variables.put("description", getProject().getDescription()); 417 variables.put("extension", "deb"); 418 variables.put("baseDir", getProject().getBasedir().getAbsolutePath()); 419 variables.put("buildDir", buildDirectory.getAbsolutePath()); 420 variables.put("project.version", getProject().getVersion()); 421 422 if (getProject().getInceptionYear() != null) { 423 variables.put("project.inceptionYear", getProject().getInceptionYear()); 424 } 425 if (getProject().getOrganization() != null) { 426 if (getProject().getOrganization().getName() != null) { 427 variables.put("project.organization.name", getProject().getOrganization().getName()); 428 } 429 if (getProject().getOrganization().getUrl() != null) { 430 variables.put("project.organization.url", getProject().getOrganization().getUrl()); 431 } 432 } 433 434 variables.put("url", getProject().getUrl()); 435 436 return new MapVariableResolver(variables); 437 } 438 439 /** 440 * Doc some cleanup and conversion on the Maven project version. 441 * <ul> 442 * <li>any "-" is replaced by "+"</li> 443 * <li>"SNAPSHOT" is replaced with the current time and date, prepended by "~"</li> 444 * </ul> 445 * 446 * @return the Maven project version 447 */ 448 private String getProjectVersion() { 449 return Utils.convertToDebianVersion(getProject().getVersion(), this.snapshotExpand, this.snapshotEnv, this.snapshotTemplate, session.getStartTime()); 450 } 451 452 /** 453 * @return whether the artifact is a POM or not 454 */ 455 private boolean isPOM() { 456 String type = getProject().getArtifact().getType(); 457 return "pom".equalsIgnoreCase(type); 458 } 459 460 /** 461 * @return whether the artifact is of configured type (i.e. the package to generate is the main artifact) 462 */ 463 private boolean isType() { 464 return type.equals(getProject().getArtifact().getType()); 465 } 466 467 /** 468 * @return whether or not Maven is currently operating in the execution root 469 */ 470 private boolean isSubmodule() { 471 // FIXME there must be a better way 472 return !session.getExecutionRootDirectory().equalsIgnoreCase(baseDir.toString()); 473 } 474 475 /** 476 * @return whether or not the main artifact was created 477 */ 478 private boolean hasMainArtifact() { 479 final MavenProject project = getProject(); 480 final Artifact artifact = project.getArtifact(); 481 return artifact.getFile() != null && artifact.getFile().isFile(); 482 } 483 484 /** 485 * Main entry point 486 * 487 * @throws MojoExecutionException on error 488 */ 489 public void execute() throws MojoExecutionException { 490 491 final MavenProject project = getProject(); 492 493 if (skip) { 494 getLog().info("skipping as configured (skip)"); 495 return; 496 } 497 498 if (skipPOMs && isPOM()) { 499 getLog().info("skipping because artifact is a pom (skipPOMs)"); 500 return; 501 } 502 503 if (skipSubmodules && isSubmodule()) { 504 getLog().info("skipping submodule (skipSubmodules)"); 505 return; 506 } 507 508 509 setData(dataSet); 510 511 console = new MojoConsole(getLog(), verbose); 512 513 initializeSignProperties(); 514 515 final VariableResolver resolver = initializeVariableResolver(new HashMap<String, String>()); 516 517 final File debFile = new File(Utils.replaceVariables(resolver, deb, openReplaceToken, closeReplaceToken)); 518 final File controlDirFile = new File(Utils.replaceVariables(resolver, controlDir, openReplaceToken, closeReplaceToken)); 519 final File installDirFile = new File(Utils.replaceVariables(resolver, installDir, openReplaceToken, closeReplaceToken)); 520 final File changesInFile = new File(Utils.replaceVariables(resolver, changesIn, openReplaceToken, closeReplaceToken)); 521 final File changesOutFile = new File(Utils.replaceVariables(resolver, changesOut, openReplaceToken, closeReplaceToken)); 522 final File changesSaveFile = new File(Utils.replaceVariables(resolver, changesSave, openReplaceToken, closeReplaceToken)); 523 final File keyringFile = keyring == null ? null : new File(Utils.replaceVariables(resolver, keyring, openReplaceToken, closeReplaceToken)); 524 525 // if there are no producers defined we try to use the artifacts 526 if (dataProducers.isEmpty()) { 527 528 if (hasMainArtifact()) { 529 Set<Artifact> artifacts = new HashSet<>(); 530 531 artifacts.add(project.getArtifact()); 532 533 @SuppressWarnings("unchecked") 534 final Set<Artifact> projectArtifacts = project.getArtifacts(); 535 536 artifacts.addAll(projectArtifacts); 537 538 @SuppressWarnings("unchecked") 539 final List<Artifact> attachedArtifacts = project.getAttachedArtifacts(); 540 541 artifacts.addAll(attachedArtifacts); 542 543 for (Artifact artifact : artifacts) { 544 final File file = artifact.getFile(); 545 if (file != null) { 546 dataProducers.add(new DataProducer() { 547 public void produce( final DataConsumer receiver ) { 548 try { 549 final File path = new File(installDirFile.getPath(), file.getName()); 550 final String entryName = path.getPath(); 551 552 final boolean symbolicLink = SymlinkUtils.isSymbolicLink(path); 553 final TarArchiveEntry e; 554 if (symbolicLink) { 555 e = new TarArchiveEntry(entryName, TarConstants.LF_SYMLINK); 556 e.setLinkName(SymlinkUtils.readSymbolicLink(path)); 557 } else { 558 e = new TarArchiveEntry(entryName, true); 559 } 560 561 e.setUserId(0); 562 e.setGroupId(0); 563 e.setUserName("root"); 564 e.setGroupName("root"); 565 e.setMode(TarEntry.DEFAULT_FILE_MODE); 566 e.setSize(file.length()); 567 568 receiver.onEachFile(new FileInputStream(file), e); 569 } catch (Exception e) { 570 getLog().error(e); 571 } 572 } 573 }); 574 } else { 575 getLog().error("No file for artifact " + artifact); 576 } 577 } 578 } 579 } 580 581 try { 582 DebMaker debMaker = new DebMaker(console, dataProducers, conffileProducers); 583 debMaker.setDeb(debFile); 584 debMaker.setControl(controlDirFile); 585 debMaker.setPackage(getProject().getArtifactId()); 586 debMaker.setDescription(getProject().getDescription()); 587 debMaker.setHomepage(getProject().getUrl()); 588 debMaker.setChangesIn(changesInFile); 589 debMaker.setChangesOut(changesOutFile); 590 debMaker.setChangesSave(changesSaveFile); 591 debMaker.setCompression(compression); 592 debMaker.setKeyring(keyringFile); 593 debMaker.setKey(key); 594 debMaker.setPassphrase(passphrase); 595 debMaker.setSignPackage(signPackage); 596 debMaker.setSignChanges(signChanges); 597 debMaker.setSignMethod(signMethod); 598 debMaker.setSignRole(signRole); 599 debMaker.setResolver(resolver); 600 debMaker.setOpenReplaceToken(openReplaceToken); 601 debMaker.setCloseReplaceToken(closeReplaceToken); 602 debMaker.setDigest(digest); 603 debMaker.setTarBigNumberMode(tarBigNumberMode); 604 debMaker.setTarLongFileMode(tarLongFileMode); 605 Long outputTimestampMs = new OutputTimestampResolver(console).resolveOutputTimestamp(outputTimestamp); 606 debMaker.setOutputTimestampMs(outputTimestampMs); 607 debMaker.validate(); 608 debMaker.makeDeb(); 609 610 // Always attach unless explicitly set to false 611 if ("true".equalsIgnoreCase(attach)) { 612 console.info("Attaching created debian package " + debFile); 613 if (!isType()) { 614 projectHelper.attachArtifact(project, type, classifier, debFile); 615 } else { 616 project.getArtifact().setFile(debFile); 617 } 618 } 619 620 } catch (PackagingException e) { 621 getLog().error("Failed to create debian package " + debFile, e); 622 throw new MojoExecutionException("Failed to create debian package " + debFile, e); 623 } 624 625 if (!isBlank(propertyPrefix)) { 626 project.getProperties().put(propertyPrefix+"version", getProjectVersion() ); 627 project.getProperties().put(propertyPrefix+"deb", debFile.getAbsolutePath()); 628 project.getProperties().put(propertyPrefix+"deb.name", debFile.getName()); 629 project.getProperties().put(propertyPrefix+"changes", changesOutFile.getAbsolutePath()); 630 project.getProperties().put(propertyPrefix+"changes.name", changesOutFile.getName()); 631 project.getProperties().put(propertyPrefix+"changes.txt", changesSaveFile.getAbsolutePath()); 632 project.getProperties().put(propertyPrefix+"changes.txt.name", changesSaveFile.getName()); 633 } 634 635 } 636 637 /** 638 * Initializes unspecified sign properties using available defaults 639 * and global settings. 640 */ 641 private void initializeSignProperties() { 642 if (!signPackage && !signChanges) { 643 return; 644 } 645 646 if (key != null && keyring != null && passphrase != null) { 647 return; 648 } 649 650 Map<String, String> properties = 651 readPropertiesFromActiveProfiles(signCfgPrefix, KEY, KEYRING, PASSPHRASE); 652 653 key = lookupIfEmpty(key, properties, KEY); 654 keyring = lookupIfEmpty(keyring, properties, KEYRING); 655 passphrase = decrypt(lookupIfEmpty(passphrase, properties, PASSPHRASE)); 656 657 if (keyring == null) { 658 try { 659 keyring = Utils.guessKeyRingFile().getAbsolutePath(); 660 console.info("Located keyring at " + keyring); 661 } catch (FileNotFoundException e) { 662 console.warn(e.getMessage()); 663 } 664 } 665 } 666 667 /** 668 * Decrypts given passphrase if needed using maven security dispatcher. 669 * See http://maven.apache.org/guides/mini/guide-encryption.html for details. 670 * 671 * @param maybeEncryptedPassphrase possibly encrypted passphrase 672 * @return decrypted passphrase 673 */ 674 private String decrypt( final String maybeEncryptedPassphrase ) { 675 if (maybeEncryptedPassphrase == null) { 676 return null; 677 } 678 679 try { 680 final String decrypted = secDispatcher.decrypt(maybeEncryptedPassphrase); 681 if (maybeEncryptedPassphrase.equals(decrypted)) { 682 console.info("Passphrase was not encrypted"); 683 } else { 684 console.info("Passphrase was successfully decrypted"); 685 } 686 return decrypted; 687 } catch (SecDispatcherException e) { 688 console.warn("Unable to decrypt passphrase: " + e.getMessage()); 689 } 690 691 return maybeEncryptedPassphrase; 692 } 693 694 /** 695 * 696 * @return the maven project used by this mojo 697 */ 698 private MavenProject getProject() { 699 if (project.getExecutionProject() != null) { 700 return project.getExecutionProject(); 701 } 702 703 return project; 704 } 705 706 707 708 /** 709 * Read properties from the active profiles. 710 * 711 * Goes through all active profiles (in the order the 712 * profiles are defined in settings.xml) and extracts 713 * the desired properties (if present). The prefix is 714 * used when looking up properties in the profile but 715 * not in the returned map. 716 * 717 * @param prefix The prefix to use or null if no prefix should be used 718 * @param properties The properties to read 719 * 720 * @return A map containing the values for the properties that were found 721 */ 722 public Map<String, String> readPropertiesFromActiveProfiles( final String prefix, 723 final String... properties ) { 724 if (settings == null) { 725 console.debug("No maven setting injected"); 726 return Collections.emptyMap(); 727 } 728 729 final List<String> activeProfilesList = settings.getActiveProfiles(); 730 if (activeProfilesList.isEmpty()) { 731 console.debug("No active profiles found"); 732 return Collections.emptyMap(); 733 } 734 735 final Map<String, String> map = new HashMap<>(); 736 final Set<String> activeProfiles = new HashSet<>(activeProfilesList); 737 738 // Iterate over all active profiles in order 739 for (final Profile profile : settings.getProfiles()) { 740 // Check if the profile is active 741 final String profileId = profile.getId(); 742 if (activeProfiles.contains(profileId)) { 743 console.debug("Trying active profile " + profileId); 744 for (final String property : properties) { 745 final String propKey = prefix != null ? prefix + property : property; 746 final String value = profile.getProperties().getProperty(propKey); 747 if (value != null) { 748 console.debug("Found property " + property + " in profile " + profileId); 749 map.put(property, value); 750 } 751 } 752 } 753 } 754 755 return map; 756 } 757 758}