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}