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>**&#47;.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}