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}