001/*
002 * Extra Bnd Repository Plugins
003 * Copyright (C) 2021  Michael N. Lipp
004 * 
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Affero General Public License as published by 
007 * the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 * 
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 
013 * for more details.
014 * 
015 * You should have received a copy of the GNU Affero General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package de.mnl.osgi.bnd.repository.maven.idxmvn;
020
021import aQute.maven.api.Archive;
022import aQute.maven.api.Revision;
023import de.mnl.osgi.bnd.maven.BoundArchive;
024import de.mnl.osgi.bnd.maven.BoundRevision;
025import de.mnl.osgi.bnd.maven.MavenVersion;
026import de.mnl.osgi.bnd.maven.MavenVersionRange;
027import java.util.ArrayList;
028import java.util.Arrays;
029import java.util.Collections;
030import java.util.Comparator;
031import java.util.HashMap;
032import java.util.List;
033import java.util.Map;
034import java.util.Optional;
035import java.util.Properties;
036import java.util.Set;
037import java.util.stream.Collectors;
038import java.util.stream.Stream;
039
040/**
041 * Class VersionSpecification.
042 */
043@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
044public class VersionSpecification {
045
046    private Type type;
047    private String artifactSpec;
048    private String extension;
049    private String classifier;
050    private MavenVersionRange range;
051
052    /**
053     * The Enum Type.
054     */
055    @SuppressWarnings("PMD.FieldNamingConventions")
056    public enum Type {
057        VERSIONS("versions"), FORCED_VERSIONS("forcedVersions"),
058        EXCLUDE("exclude");
059
060        @SuppressWarnings("PMD.UseConcurrentHashMap")
061        private static final Map<String, Type> types = new HashMap<>();
062        static {
063            Stream.of(values()).forEach(v -> types.put(v.keyword, v));
064        }
065
066        /** The keyword. */
067        public final String keyword;
068
069        Type(String keyword) {
070            this.keyword = keyword;
071        }
072
073        /**
074         * Checks if is keyword.
075         *
076         * @param value the value
077         * @return true, if is keyword
078         */
079        public static boolean isKeyword(String value) {
080            return types.containsKey(value);
081        }
082
083        /**
084         * Of.
085         *
086         * @param value the value
087         * @return the type
088         */
089        @SuppressWarnings("PMD.ShortMethodName")
090        public static Type of(String value) {
091            return Optional.ofNullable(types.get(value)).orElseThrow();
092        }
093    }
094
095    /**
096     * Parses the.
097     *
098     * @param props the props
099     * @return the version specification[]
100     */
101    @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops",
102        "PMD.ImplicitSwitchFallThrough" })
103    public static VersionSpecification[] parse(Properties props) {
104        List<VersionSpecification> result = new ArrayList<>();
105        for (var key : props.keySet()) {
106            String entry = (String) key;
107            String[] entryParts = entry.split(";");
108            if (!Type.isKeyword(entryParts[entryParts.length - 1])) {
109                continue;
110            }
111            var spec = new VersionSpecification();
112            spec.type = Type.of(entryParts[entryParts.length - 1]);
113            String[] nameParts = entryParts.length == 1 ? new String[0]
114                : entryParts[0].split(":");
115            switch (nameParts.length) {
116            case 3:
117                spec.classifier = nameParts[2];
118                // fallthrough
119            case 2:
120                spec.extension = nameParts[1];
121                // fallthrough
122            case 1:
123                spec.artifactSpec = nameParts[0];
124                break;
125            default:
126                break;
127            }
128            spec.range
129                = MavenVersionRange.parseRange(props.getProperty(entry));
130            result.add(spec);
131        }
132        return result.toArray(new VersionSpecification[0]);
133    }
134
135    /**
136     * Gets the type.
137     *
138     * @return the type
139     */
140    public Type getType() {
141        return type;
142    }
143
144    /**
145     * Match a revision against the version specifications and return
146     * the archives that are matches by any specification.
147     *
148     * @param specs the specs
149     * @param revision the revision
150     * @return the sets the result
151     */
152    public static Set<Archive> toSelected(VersionSpecification[] specs,
153            Revision revision) {
154        return Arrays.stream(specs)
155            // find applicable versions specifications
156            .filter(s -> Set
157                .of(VersionSpecification.Type.VERSIONS,
158                    VersionSpecification.Type.FORCED_VERSIONS)
159                .contains(s.getType()) && s.matches(revision.artifact))
160            // sort by artifact spec's length descending
161            .sorted(Comparator.comparingInt(
162                (VersionSpecification vs) -> Optional
163                    .ofNullable(vs.artifactSpec).orElse("").length())
164                .reversed())
165            .findFirst().map(s -> {
166                // check if best match includes the revision
167                if (s.range.includes(MavenVersion.from(revision.version))) {
168                    return Set.of(
169                        new Archive(revision, null, s.extension, s.classifier));
170                }
171                return Collections.<Archive> emptySet();
172            }).orElse(Set.of(new Archive(revision, null, null, null)));
173    }
174
175    /**
176     * Match a revision against the version specifications and return
177     * the archives that are matches by any specification.
178     *
179     * @param specs the specs
180     * @param revision the revision
181     * @return the sets the result
182     */
183    public static Set<BoundArchive> toSelected(VersionSpecification[] specs,
184            BoundRevision revision) {
185        return toSelected(specs, revision.unbound()).stream()
186            .map(archive -> new BoundArchive(revision.mavenBackingRepository(),
187                archive))
188            .collect(Collectors.toSet());
189    }
190
191    /**
192     * Checks if is forced.
193     *
194     * @param specs the specs
195     * @param archive the archive
196     * @return true, if is forced
197     */
198    public static boolean isForced(VersionSpecification[] specs,
199            Archive archive) {
200        return Arrays.stream(specs)
201            .filter(s -> s.getType() == Type.FORCED_VERSIONS
202                && s.matches(archive.revision.artifact)
203                && s.range
204                    .includes(MavenVersion.from(archive.revision.version)))
205            .findAny().isPresent();
206    }
207
208    /**
209     * Match a revision against the version specifications and return
210     * the archives that are matches by any specification.
211     *
212     * @param specs the specs
213     * @param artifact the artifact name
214     * @return the sets the result
215     */
216    public static MavenVersionRange excluded(VersionSpecification[] specs,
217            String artifact) {
218        return Arrays.stream(specs).filter(s -> s.getType() == Type.EXCLUDE
219            && s.matches(artifact))
220            .map(s -> s.range).findFirst().orElse(MavenVersionRange.NONE);
221    }
222
223    /**
224     * Match the given artifact name against the specification.
225     *
226     * @param name the name
227     * @return true, if it matches
228     */
229    public boolean matches(String name) {
230        // Try any and name unmodified
231        if (artifactSpec == null || artifactSpec.equals(name)) {
232            return true;
233        }
234        // Try <name>.*, successively removing trailing parts.
235        String rest = name;
236        while (true) {
237            if (artifactSpec.equals(rest + ".*")) {
238                return true;
239            }
240            int lastDot = rest.lastIndexOf('.');
241            if (lastDot < 0) {
242                break;
243            }
244            rest = rest.substring(0, lastDot);
245        }
246        return false;
247    }
248
249    /*
250     * (non-Javadoc)
251     * 
252     * @see java.lang.Object#toString()
253     */
254    @Override
255    public String toString() {
256        StringBuilder result = new StringBuilder();
257        if (artifactSpec != null) {
258            result.append(artifactSpec);
259        }
260        if (extension != null) {
261            result.append(':').append(extension);
262        }
263        if (classifier != null) {
264            result.append(':').append(classifier);
265        }
266        if (result.length() > 0) {
267            result.append(';');
268        }
269        result.append(type).append('=').append(range);
270        return result.toString();
271    }
272}