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
063        static {
064            Stream.of(values()).forEach(v -> types.put(v.keyword, v));
065        }
066
067        /** The keyword. */
068        public final String keyword;
069
070        Type(String keyword) {
071            this.keyword = keyword;
072        }
073
074        /**
075         * Checks if is keyword.
076         *
077         * @param value the value
078         * @return true, if is keyword
079         */
080        public static boolean isKeyword(String value) {
081            return types.containsKey(value);
082        }
083
084        /**
085         * Of.
086         *
087         * @param value the value
088         * @return the type
089         */
090        @SuppressWarnings("PMD.ShortMethodName")
091        public static Type of(String value) {
092            return Optional.ofNullable(types.get(value)).orElseThrow();
093        }
094    }
095
096    /**
097     * Parses the.
098     *
099     * @param props the props
100     * @return the version specification[]
101     */
102    @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops",
103        "PMD.ImplicitSwitchFallThrough" })
104    public static VersionSpecification[] parse(Properties props) {
105        List<VersionSpecification> result = new ArrayList<>();
106        for (var key : props.keySet()) {
107            String entry = (String) key;
108            String[] entryParts = entry.split(";");
109            if (!Type.isKeyword(entryParts[entryParts.length - 1])) {
110                continue;
111            }
112            var spec = new VersionSpecification();
113            spec.type = Type.of(entryParts[entryParts.length - 1]);
114            String[] nameParts = entryParts.length == 1 ? new String[0]
115                : entryParts[0].split(":");
116            switch (nameParts.length) {
117            case 3:
118                spec.classifier = nameParts[2];
119                // fallthrough
120            case 2:
121                spec.extension = nameParts[1];
122                // fallthrough
123            case 1:
124                spec.artifactSpec = nameParts[0];
125                break;
126            default:
127                break;
128            }
129            spec.range
130                = MavenVersionRange.parseRange(props.getProperty(entry));
131            result.add(spec);
132        }
133        return result.toArray(new VersionSpecification[0]);
134    }
135
136    /**
137     * Gets the type.
138     *
139     * @return the type
140     */
141    public Type getType() {
142        return type;
143    }
144
145    /**
146     * Match a revision against the version specifications and return
147     * the archives that are matches by any specification.
148     *
149     * @param specs the specs
150     * @param revision the revision
151     * @return the sets the result
152     */
153    public static Set<Archive> toSelected(VersionSpecification[] specs,
154            Revision revision) {
155        var mvnVer = MavenVersion.from(revision.version);
156        return Arrays.stream(specs)
157            // find applicable versions specifications
158            .filter(s -> Set
159                .of(Type.VERSIONS, Type.FORCED_VERSIONS)
160                .contains(s.getType()) && s.matches(revision.artifact))
161            // sort by artifact spec's length descending
162            .sorted(Comparator.comparingInt(
163                (VersionSpecification vs) -> Optional
164                    .ofNullable(vs.artifactSpec).orElse("").length())
165                .reversed())
166            .findFirst().map(s -> {
167                // Check if best match includes the given revision
168                // and the revision is forced or not excluded
169                if (s.range.includes(mvnVer)
170                    && (s.getType() == Type.FORCED_VERSIONS
171                        || !excluded(specs, revision.artifact)
172                            .includes(mvnVer))) {
173                    return Set.of(
174                        new Archive(revision, null, s.extension, s.classifier));
175                }
176                return Collections.<Archive> emptySet();
177            }).orElseGet(() -> {
178                // Check if revision is not excluded
179                if (!excluded(specs, revision.artifact).includes(mvnVer)) {
180                    return Set.of(new Archive(revision, null, null, null));
181                }
182                return Collections.<Archive> emptySet();
183            });
184    }
185
186    /**
187     * Match a revision against the version specifications and return
188     * the archives that are matches by any specification.
189     *
190     * @param specs the specs
191     * @param revision the revision
192     * @return the sets the result
193     */
194    public static Set<BoundArchive> toSelected(VersionSpecification[] specs,
195            BoundRevision revision) {
196        return toSelected(specs, revision.unbound()).stream()
197            .map(archive -> new BoundArchive(revision.mavenBackingRepository(),
198                archive))
199            .collect(Collectors.toSet());
200    }
201
202    /**
203     * Checks if is forced.
204     *
205     * @param specs the specs
206     * @param archive the archive
207     * @return true, if is forced
208     */
209    public static boolean isForced(VersionSpecification[] specs,
210            Archive archive) {
211        return Arrays.stream(specs)
212            .filter(s -> s.getType() == Type.FORCED_VERSIONS
213                && s.matches(archive.revision.artifact)
214                && s.range
215                    .includes(MavenVersion.from(archive.revision.version)))
216            .findAny().isPresent();
217    }
218
219    /**
220     * Find the exclude version range for the artifact with the given
221     * name. Return {@code MavenVersionRange.NONE} if there is no
222     * exclude defined.
223     *
224     * @param specs the specs
225     * @param artifact the artifact name
226     * @return the sets the result
227     */
228    public static MavenVersionRange excluded(VersionSpecification[] specs,
229            String artifact) {
230        return Arrays.stream(specs).filter(s -> s.getType() == Type.EXCLUDE
231            && s.matches(artifact))
232            .map(s -> s.range).findFirst().orElse(MavenVersionRange.NONE);
233    }
234
235    /**
236     * Match the given artifact name against the specification.
237     *
238     * @param name the name
239     * @return true, if it matches
240     */
241    public boolean matches(String name) {
242        // Try any and name unmodified
243        if (artifactSpec == null || artifactSpec.equals(name)) {
244            return true;
245        }
246        // Try <name>.*, successively removing trailing parts.
247        String rest = name;
248        while (true) {
249            if (artifactSpec.equals(rest + ".*")) {
250                return true;
251            }
252            int lastDot = rest.lastIndexOf('.');
253            if (lastDot < 0) {
254                break;
255            }
256            rest = rest.substring(0, lastDot);
257        }
258        return false;
259    }
260
261    /*
262     * (non-Javadoc)
263     * 
264     * @see java.lang.Object#toString()
265     */
266    @Override
267    public String toString() {
268        StringBuilder result = new StringBuilder();
269        if (artifactSpec != null) {
270            result.append(artifactSpec);
271        }
272        if (extension != null) {
273            result.append(':').append(extension);
274        }
275        if (classifier != null) {
276            result.append(':').append(classifier);
277        }
278        if (result.length() > 0) {
279            result.append(';');
280        }
281        result.append(type).append('=').append(range);
282        return result.toString();
283    }
284}