blob: a990f5139d7143923047587596d6f67fcdccc67d [file] [log] [blame]
Stuart McCullochf29ef3f2008-12-04 07:58:07 +00001/* Copyright 2006 aQute SARL
2 * Licensed under the Apache License, Version 2.0, see http://www.apache.org/licenses/LICENSE-2.0 */
3package aQute.lib.osgi;
4
5import java.io.*;
6import java.util.*;
7import java.util.jar.*;
8import java.util.regex.*;
9
10import aQute.libg.qtokens.*;
11
12public class Verifier extends Analyzer {
13
14 Jar dot;
15 Manifest manifest;
16 Map<String, Map<String, String>> referred = newHashMap();
17 Map<String, Map<String, String>> contained = newHashMap();
18 Map<String, Set<String>> uses = newHashMap();
19 Map<String, Map<String, String>> mimports;
20 Map<String, Map<String, String>> mdynimports;
21 Map<String, Map<String, String>> mexports;
22 List<Jar> bundleClasspath;
23 Map<String, Map<String, String>> ignore = newHashMap(); // Packages
24 // to
25 // ignore
26
27 Map<String, Clazz> classSpace;
28 boolean r3;
29 boolean usesRequire;
30 boolean fragment;
31 Attributes main;
32
33 final static Pattern EENAME = Pattern
34 .compile("CDC-1\\.0/Foundation-1\\.0"
35 + "|CDC-1\\.1/Foundation-1\\.1"
Stuart McCullocha75a5152009-01-29 07:46:54 +000036 + "|OSGi/Minimum-1\\.[1-9]"
Stuart McCullochf29ef3f2008-12-04 07:58:07 +000037 + "|JRE-1\\.1"
38 + "|J2SE-1\\.2"
39 + "|J2SE-1\\.3"
40 + "|J2SE-1\\.4"
41 + "|J2SE-1\\.5"
Stuart McCullocha75a5152009-01-29 07:46:54 +000042 + "|JavaSE-1\\.6"
Stuart McCullochf29ef3f2008-12-04 07:58:07 +000043 + "|PersonalJava-1\\.1"
44 + "|PersonalJava-1\\.2"
45 + "|CDC-1\\.0/PersonalBasis-1\\.0"
46 + "|CDC-1\\.0/PersonalJava-1\\.0");
47
48 final static Pattern BUNDLEMANIFESTVERSION = Pattern
49 .compile("2");
50 public final static String SYMBOLICNAME_STRING = "[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*";
51 public final static Pattern SYMBOLICNAME = Pattern
52 .compile(SYMBOLICNAME_STRING);
53
54 public final static String VERSION_STRING = "[0-9]+(\\.[0-9]+(\\.[0-9]+(\\.[0-9A-Za-z_-]+)?)?)?";
55 public final static Pattern VERSION = Pattern
56 .compile(VERSION_STRING);
57 final static Pattern FILTEROP = Pattern
58 .compile("=|<=|>=|~=");
59 final static Pattern VERSIONRANGE = Pattern
60 .compile("((\\(|\\[)"
61 + VERSION_STRING
62 + ","
63 + VERSION_STRING
64 + "(\\]|\\)))|"
65 + VERSION_STRING);
66 final static Pattern FILE = Pattern
67 .compile("/?[^/\"\n\r\u0000]+(/[^/\"\n\r\u0000]+)*");
68 final static Pattern WILDCARDPACKAGE = Pattern
69 .compile("((\\p{Alnum}|_)+(\\.(\\p{Alnum}|_)+)*(\\.\\*)?)|\\*");
70 final static Pattern ISO639 = Pattern
71 .compile("[A-Z][A-Z]");
72 public static Pattern HEADER_PATTERN = Pattern
73 .compile("[A-Za-z0-9][-a-zA-Z0-9_]+");
74 public static Pattern TOKEN = Pattern
75 .compile("[-a-zA-Z0-9_]+");
76
77 Properties properties;
78
79 public Verifier(Jar jar) throws Exception {
80 this(jar, null);
81 }
82
83 public Verifier(Jar jar, Properties properties) throws Exception {
84 this.dot = jar;
85 this.properties = properties;
86 this.manifest = jar.getManifest();
87 if (manifest == null) {
88 manifest = new Manifest();
89 error("This file contains no manifest and is therefore not a bundle");
90 }
91 main = this.manifest.getMainAttributes();
92 verifyHeaders(main);
93 r3 = getHeader(Analyzer.BUNDLE_MANIFESTVERSION) == null;
94 usesRequire = getHeader(Analyzer.REQUIRE_BUNDLE) != null;
95 fragment = getHeader(Analyzer.FRAGMENT_HOST) != null;
96
97 bundleClasspath = getBundleClassPath();
98 mimports = parseHeader(manifest.getMainAttributes().getValue(
99 Analyzer.IMPORT_PACKAGE));
100 mdynimports = parseHeader(manifest.getMainAttributes().getValue(
101 Analyzer.DYNAMICIMPORT_PACKAGE));
102 mexports = parseHeader(manifest.getMainAttributes().getValue(
103 Analyzer.EXPORT_PACKAGE));
104
105 ignore = parseHeader(manifest.getMainAttributes().getValue(
106 Analyzer.IGNORE_PACKAGE));
107 }
108
109 public Verifier() {
110 // TODO Auto-generated constructor stub
111 }
112
113 private void verifyHeaders(Attributes main) {
114 for (Object element : main.keySet()) {
115 Attributes.Name header = (Attributes.Name) element;
116 String h = header.toString();
117 if (!HEADER_PATTERN.matcher(h).matches())
118 error("Invalid Manifest header: " + h + ", pattern="
119 + HEADER_PATTERN);
120 }
121 }
122
123 private List<Jar> getBundleClassPath() {
124 List<Jar> list = newList();
125 String bcp = getHeader(Analyzer.BUNDLE_CLASSPATH);
126 if (bcp == null) {
127 list.add(dot);
128 } else {
129 Map<String,Map<String,String>> entries = parseHeader(bcp);
130 for (String jarOrDir : entries.keySet()) {
131 if (jarOrDir.equals(".")) {
132 list.add(dot);
133 } else {
134 if (jarOrDir.equals("/"))
135 jarOrDir = "";
136 if (jarOrDir.endsWith("/")) {
137 error("Bundle-Classpath directory must not end with a slash: "
138 + jarOrDir);
139 jarOrDir = jarOrDir.substring(0, jarOrDir.length() - 1);
140 }
141
142 Resource resource = dot.getResource(jarOrDir);
143 if (resource != null) {
144 try {
145 Jar sub = new Jar(jarOrDir);
146 addClose(sub);
147 EmbeddedResource.build(sub, resource);
148 if (!jarOrDir.endsWith(".jar"))
149 warning("Valid JAR file on Bundle-Classpath does not have .jar extension: "
150 + jarOrDir);
151 list.add(sub);
152 } catch (Exception e) {
153 error("Invalid embedded JAR file on Bundle-Classpath: "
154 + jarOrDir + ", " + e);
155 }
156 } else if (dot.getDirectories().containsKey(jarOrDir)) {
157 if (r3)
158 error("R3 bundles do not support directories on the Bundle-ClassPath: "
159 + jarOrDir);
160
161 try {
162 Jar sub = new Jar(jarOrDir);
Stuart McCulloch66469e02008-12-04 08:57:54 +0000163 addClose(sub);
Stuart McCullocha75a5152009-01-29 07:46:54 +0000164 for ( Map.Entry<String, Resource> entry : dot.getResources().entrySet()) {
165 if ( entry.getKey().startsWith(jarOrDir))
166 sub.putResource( entry.getKey().substring(jarOrDir.length()+1), entry.getValue());
167 }
Stuart McCullochf29ef3f2008-12-04 07:58:07 +0000168 list.add(sub);
169 } catch (Exception e) {
170 error("Invalid embedded directory file on Bundle-Classpath: "
171 + jarOrDir + ", " + e);
172 }
173 } else {
174 error("Cannot find a file or directory for Bundle-Classpath entry: "
175 + jarOrDir);
176 }
177 }
178 }
179 }
180 return list;
181 }
182
183 /*
184 * Bundle-NativeCode ::= nativecode ( ',' nativecode )* ( ’,’ optional) ?
185 * nativecode ::= path ( ';' path )* // See 1.4.2 ( ';' parameter )+
186 * optional ::= ’*’
187 */
188 public void verifyNative() {
189 String nc = getHeader("Bundle-NativeCode");
190 doNative(nc);
191 }
192
193 public void doNative(String nc) {
194 if (nc != null) {
195 QuotedTokenizer qt = new QuotedTokenizer(nc, ",;=", false);
196 char del;
197 do {
198 do {
199 String name = qt.nextToken();
200 if (name == null) {
201 error("Can not parse name from bundle native code header: "
202 + nc);
203 return;
204 }
205 del = qt.getSeparator();
206 if (del == ';') {
207 if (dot != null && !dot.exists(name)) {
208 error("Native library not found in JAR: " + name);
209 }
210 } else {
211 String value = null;
212 if (del == '=')
213 value = qt.nextToken();
214
215 String key = name.toLowerCase();
216 if (key.equals("osname")) {
217 // ...
218 } else if (key.equals("osversion")) {
219 // verify version range
220 verify(value, VERSIONRANGE);
221 } else if (key.equals("language")) {
222 verify(value, ISO639);
223 } else if (key.equals("processor")) {
224 // verify(value, PROCESSORS);
225 } else if (key.equals("selection-filter")) {
226 // verify syntax filter
227 verifyFilter(value);
228 } else if (name.equals("*") && value == null) {
229 // Wildcard must be at end.
230 if (qt.nextToken() != null)
231 error("Bundle-Native code header may only END in wildcard: nc");
232 } else {
233 warning("Unknown attribute in native code: " + name
234 + "=" + value);
235 }
236 del = qt.getSeparator();
237 }
238 } while (del == ';');
239 } while (del == ',');
240 }
241 }
242
243 public void verifyFilter(String value) {
244 try {
245 verifyFilter(value, 0);
246 } catch (Exception e) {
247 error("Not a valid filter: " + value + e.getMessage());
248 }
249 }
250
251 private void verifyActivator() {
252 String bactivator = getHeader("Bundle-Activator");
253 if (bactivator != null) {
254 Clazz cl = loadClass(bactivator);
255 if (cl == null) {
256 int n = bactivator.lastIndexOf('.');
257 if (n > 0) {
258 String pack = bactivator.substring(0, n);
259 if (mimports.containsKey(pack))
260 return;
261 error("Bundle-Activator not found on the bundle class path nor in imports: "
262 + bactivator);
263 } else
264 error("Activator uses default package and is not local (default package can not be imported): "
265 + bactivator);
266 }
267 }
268 }
269
270 private Clazz loadClass(String className) {
271 String path = className.replace('.', '/') + ".class";
272 return (Clazz) classSpace.get(path);
273 }
274
275 private void verifyComponent() {
276 String serviceComponent = getHeader("Service-Component");
277 if (serviceComponent != null) {
278 Map<String,Map<String,String>> map = parseHeader(serviceComponent);
279 for (String component : map.keySet()) {
280 if (!dot.exists(component)) {
281 error("Service-Component entry can not be located in JAR: "
282 + component);
283 } else {
284 // validate component ...
285 }
286 }
287 }
288 }
289
290 public void info() {
291 System.out.println("Refers : " + referred);
292 System.out.println("Contains : " + contained);
293 System.out.println("Manifest Imports : " + mimports);
294 System.out.println("Manifest Exports : " + mexports);
295 }
296
297 /**
298 * Invalid exports are exports mentioned in the manifest but not found on
299 * the classpath. This can be calculated with: exports - contains.
300 *
301 * Unfortunately, we also must take duplicate names into account. These
302 * duplicates are of course no erroneous.
303 */
304 private void verifyInvalidExports() {
305 Set<String> invalidExport = newSet(mexports.keySet());
306 invalidExport.removeAll(contained.keySet());
307
308 // We might have duplicate names that are marked for it. These
309 // should not be counted. Should we test them against the contained
310 // set? Hmm. If someone wants to hang himself by using duplicates than
311 // I guess he can go ahead ... This is not a recommended practice
312 for (Iterator<String> i = invalidExport.iterator(); i.hasNext();) {
313 String pack = i.next();
314 if (isDuplicate(pack))
315 i.remove();
316 }
317
318 if (!invalidExport.isEmpty())
319 error("Exporting packages that are not on the Bundle-Classpath"
320 + bundleClasspath + ": " + invalidExport);
321 }
322
323 /**
324 * Invalid imports are imports that we never refer to. They can be
325 * calculated by removing the refered packages from the imported packages.
326 * This leaves packages that the manifest imported but that we never use.
327 */
328 private void verifyInvalidImports() {
329 Set<String> invalidImport = newSet(mimports.keySet());
330 invalidImport.removeAll(referred.keySet());
331 // TODO Added this line but not sure why it worked before ...
332 invalidImport.removeAll(contained.keySet());
333 String bactivator = getHeader(Analyzer.BUNDLE_ACTIVATOR);
334 if (bactivator != null) {
335 int n = bactivator.lastIndexOf('.');
336 if (n > 0) {
337 invalidImport.remove(bactivator.substring(0, n));
338 }
339 }
340 if (isPedantic() && !invalidImport.isEmpty())
341 warning("Importing packages that are never refered to by any class on the Bundle-Classpath"
342 + bundleClasspath + ": " + invalidImport);
343 }
344
345 /**
346 * Check for unresolved imports. These are referals that are not imported by
347 * the manifest and that are not part of our bundle classpath. The are
348 * calculated by removing all the imported packages and contained from the
349 * refered packages.
350 */
351 private void verifyUnresolvedReferences() {
352 Set<String> unresolvedReferences = new TreeSet<String>(referred
353 .keySet());
354 unresolvedReferences.removeAll(mimports.keySet());
355 unresolvedReferences.removeAll(contained.keySet());
356
357 // Remove any java.** packages.
358 for (Iterator<String> p = unresolvedReferences.iterator(); p.hasNext();) {
359 String pack = p.next();
360 if (pack.startsWith("java.") || ignore.containsKey(pack))
361 p.remove();
362 else {
363 // Remove any dynamic imports
364 if (isDynamicImport(pack))
365 p.remove();
366 }
367 }
368
369 if (!unresolvedReferences.isEmpty()) {
370 // Now we want to know the
371 // classes that are the culprits
372 Set<String> culprits = new HashSet<String>();
373 for (Clazz clazz : classSpace.values()) {
374 if (hasOverlap(unresolvedReferences, clazz.imports.keySet()))
375 culprits.add(clazz.getPath());
376 }
377
378 error("Unresolved references to " + unresolvedReferences
379 + " by class(es) on the Bundle-Classpath" + bundleClasspath
380 + ": " + culprits);
381 }
382 }
383
384 /**
385 * @param p
386 * @param pack
387 */
388 private boolean isDynamicImport(String pack) {
389 for (String pattern : mdynimports.keySet()) {
390 // Wildcard?
391 if (pattern.equals("*"))
392 return true; // All packages can be dynamically imported
393
394 if (pattern.endsWith(".*")) {
395 pattern = pattern.substring(0, pattern.length() - 2);
396 if (pack.startsWith(pattern)
397 && (pack.length() == pattern.length() || pack
398 .charAt(pattern.length()) == '.'))
399 return true;
400 } else {
401 if (pack.equals(pattern))
402 return true;
403 }
404 }
405 return false;
406 }
407
408 private boolean hasOverlap(Set<?> a, Set<?> b) {
409 for (Iterator<?> i = a.iterator(); i.hasNext();) {
410 if (b.contains(i.next()))
411 return true;
412 }
413 return false;
414 }
415
416 public void verify() throws IOException {
417 if (classSpace == null)
418 classSpace = analyzeBundleClasspath(dot,
419 parseHeader(getHeader(Analyzer.BUNDLE_CLASSPATH)),
420 contained, referred, uses);
421 verifyManifestFirst();
422 verifyActivator();
423 verifyComponent();
424 verifyNative();
425 verifyInvalidExports();
426 verifyInvalidImports();
427 verifyUnresolvedReferences();
428 verifySymbolicName();
429 verifyListHeader("Bundle-RequiredExecutionEnvironment", EENAME, false);
430 verifyHeader("Bundle-ManifestVersion", BUNDLEMANIFESTVERSION, false);
431 verifyHeader("Bundle-Version", VERSION, true);
432 verifyListHeader("Bundle-Classpath", FILE, false);
433 verifyDynamicImportPackage();
Stuart McCullocha75a5152009-01-29 07:46:54 +0000434 verifyBundleClasspath();
Stuart McCullochf29ef3f2008-12-04 07:58:07 +0000435 if (usesRequire) {
436 if (!getErrors().isEmpty()) {
437 getWarnings()
438 .add(
439 0,
440 "Bundle uses Require Bundle, this can generate false errors because then not enough information is available without the required bundles");
441 }
442 }
443 }
444
Stuart McCullocha75a5152009-01-29 07:46:54 +0000445 public void verifyBundleClasspath() {
446 Map<String,Map<String,String>> bcp = parseHeader(getHeader(Analyzer.BUNDLE_CLASSPATH));
447 if ( bcp.isEmpty() || bcp.containsKey("."))
448 return;
449
450 for ( String path : dot.getResources().keySet()) {
451 if ( path.endsWith(".class")) {
452 warning("The Bundle-Classpath does not contain the actual bundle JAR (as specified with '.' in the Bundle-Classpath) but the JAR does contain classes. Is this intentional?");
453 return;
454 }
455 }
456 }
457
Stuart McCullochf29ef3f2008-12-04 07:58:07 +0000458 /**
459 * <pre>
460 * DynamicImport-Package ::= dynamic-description
461 * ( ',' dynamic-description )*
462 *
463 * dynamic-description::= wildcard-names ( ';' parameter )*
464 * wildcard-names ::= wildcard-name ( ';' wildcard-name )*
465 * wildcard-name ::= package-name
466 * | ( package-name '.*' ) // See 1.4.2
467 * | '*'
468 * </pre>
469 */
470 private void verifyDynamicImportPackage() {
471 verifyListHeader("DynamicImport-Package", WILDCARDPACKAGE, true);
472 String dynamicImportPackage = getHeader("DynamicImport-Package");
473 if (dynamicImportPackage == null)
474 return;
475
476 Map<String, Map<String,String>> map = parseHeader(dynamicImportPackage);
477 for (String name : map.keySet()) {
478 name = name.trim();
479 if (!verify(name, WILDCARDPACKAGE))
480 error("DynamicImport-Package header contains an invalid package name: "
481 + name);
482
483 Map<String,String> sub = map.get(name);
484 if (r3 && sub.size() != 0) {
485 error("DynamicPackage-Import has attributes on import: "
486 + name
487 + ". This is however, an <=R3 bundle and attributes on this header were introduced in R4. ");
488 }
489 }
490 }
491
492 private void verifyManifestFirst() {
493 if (!dot.manifestFirst) {
494 error("Invalid JAR stream: Manifest should come first to be compatible with JarInputStream, it was not");
495 }
496 }
497
498 private void verifySymbolicName() {
499 Map<String,Map<String,String>> bsn = parseHeader(getHeader(Analyzer.BUNDLE_SYMBOLICNAME));
500 if (!bsn.isEmpty()) {
501 if (bsn.size() > 1)
502 error("More than one BSN specified " + bsn);
503
504 String name = (String) bsn.keySet().iterator().next();
505 if (!SYMBOLICNAME.matcher(name).matches()) {
506 error("Symbolic Name has invalid format: " + name);
507 }
508 }
509 }
510
511 /**
512 * <pre>
513 * filter ::= ’(’ filter-comp ’)’
514 * filter-comp ::= and | or | not | operation
515 * and ::= ’&amp;’ filter-list
516 * or ::= ’|’ filter-list
517 * not ::= ’!’ filter
518 * filter-list ::= filter | filter filter-list
519 * operation ::= simple | present | substring
520 * simple ::= attr filter-type value
521 * filter-type ::= equal | approx | greater | less
522 * equal ::= ’=’
523 * approx ::= ’&tilde;=’
524 * greater ::= ’&gt;=’
525 * less ::= ’&lt;=’
526 * present ::= attr ’=*’
527 * substring ::= attr ’=’ initial any final
528 * inital ::= () | value
529 * any ::= ’*’ star-value
530 * star-value ::= () | value ’*’ star-value
531 * final ::= () | value
532 * value ::= &lt;see text&gt;
533 * </pre>
534 *
535 * @param expr
536 * @param index
537 * @return
538 */
539
540 int verifyFilter(String expr, int index) {
541 try {
542 while (Character.isWhitespace(expr.charAt(index)))
543 index++;
544
545 if (expr.charAt(index) != '(')
546 throw new IllegalArgumentException(
547 "Filter mismatch: expected ( at position " + index
548 + " : " + expr);
549
550 index++;
551 while (Character.isWhitespace(expr.charAt(index)))
552 index++;
553
554 switch (expr.charAt(index)) {
555 case '!':
556 case '&':
557 case '|':
558 return verifyFilterSubExpression(expr, index) + 1;
559
560 default:
561 return verifyFilterOperation(expr, index) + 1;
562 }
563 } catch (IndexOutOfBoundsException e) {
564 throw new IllegalArgumentException(
565 "Filter mismatch: early EOF from " + index);
566 }
567 }
568
569 private int verifyFilterOperation(String expr, int index) {
570 StringBuffer sb = new StringBuffer();
571 while ("=><~()".indexOf(expr.charAt(index)) < 0) {
572 sb.append(expr.charAt(index++));
573 }
574 String attr = sb.toString().trim();
575 if (attr.length() == 0)
576 throw new IllegalArgumentException(
577 "Filter mismatch: attr at index " + index + " is 0");
578 sb = new StringBuffer();
579 while ("=><~".indexOf(expr.charAt(index)) >= 0) {
580 sb.append(expr.charAt(index++));
581 }
582 String operator = sb.toString();
583 if (!verify(operator, FILTEROP))
584 throw new IllegalArgumentException(
585 "Filter error, illegal operator " + operator + " at index "
586 + index);
587
588 sb = new StringBuffer();
589 while (")".indexOf(expr.charAt(index)) < 0) {
590 switch (expr.charAt(index)) {
591 case '\\':
592 if (expr.charAt(index + 1) == '*'
593 || expr.charAt(index + 1) == ')')
594 index++;
595 else
596 throw new IllegalArgumentException(
597 "Filter error, illegal use of backslash at index "
598 + index
599 + ". Backslash may only be used before * or (");
600 }
601 sb.append(expr.charAt(index++));
602 }
603 return index;
604 }
605
606 private int verifyFilterSubExpression(String expr, int index) {
607 do {
608 index = verifyFilter(expr, index + 1);
609 while (Character.isWhitespace(expr.charAt(index)))
610 index++;
611 if (expr.charAt(index) != ')')
612 throw new IllegalArgumentException(
613 "Filter mismatch: expected ) at position " + index
614 + " : " + expr);
615 index++;
616 } while (expr.charAt(index) == '(');
617 return index;
618 }
619
620 private String getHeader(String string) {
621 return main.getValue(string);
622 }
623
624 @SuppressWarnings("unchecked")
625 private boolean verifyHeader(String name, Pattern regex, boolean error) {
626 String value = manifest.getMainAttributes().getValue(name);
627 if (value == null)
628 return false;
629
630 QuotedTokenizer st = new QuotedTokenizer(value.trim(), ",");
631 for (Iterator<String> i = st.getTokenSet().iterator(); i.hasNext();) {
632 if (!verify((String) i.next(), regex)) {
633 String msg = "Invalid value for " + name + ", " + value
634 + " does not match " + regex.pattern();
635 if (error)
636 error(msg);
637 else
638 warning(msg);
639 }
640 }
641 return true;
642 }
643
644 private boolean verify(String value, Pattern regex) {
645 return regex.matcher(value).matches();
646 }
647
648 private boolean verifyListHeader(String name, Pattern regex, boolean error) {
649 String value = manifest.getMainAttributes().getValue(name);
650 if (value == null)
651 return false;
652
653 Map<String,Map<String,String>> map = parseHeader(value);
654 for (String header : map.keySet()) {
655 if (!regex.matcher(header).matches()) {
656 String msg = "Invalid value for " + name + ", " + value
657 + " does not match " + regex.pattern();
658 if (error)
659 error(msg);
660 else
661 warning(msg);
662 }
663 }
664 return true;
665 }
666
667 public String getProperty(String key, String deflt) {
668 if (properties == null)
669 return deflt;
670 return properties.getProperty(key, deflt);
671 }
672
673 public void setClassSpace(Map<String,Clazz> classspace,
674 Map<String, Map<String, String>> contained,
675 Map<String, Map<String, String>> referred,
676 Map<String, Set<String>> uses) {
677 this.classSpace = classspace;
678 this.contained = contained;
679 this.referred = referred;
680 this.uses = uses;
681 }
682
683 public static boolean isVersion(String version) {
684 return VERSION.matcher(version).matches();
685 }
686
687}