1 package eu.simuline.m2latex.core;
2
3 //import org.apache.maven.project.io.xpp3.MavenXpp3Reader;
4 //import org.apache.maven.project.Model;
5
6 import eu.simuline.m2latex.core.CommandExecutor.CmdResult;
7
8 import java.io.InputStream;
9 import java.io.IOException;
10
11 import java.util.Properties;
12 import java.util.SortedSet;
13 import java.util.TreeSet;
14 import java.util.ArrayList;
15 import java.util.List;
16 import java.util.regex.Matcher;
17 import java.util.regex.Pattern;
18
19 import java.util.jar.Attributes;
20 import java.util.jar.Manifest;
21
22 // TBD: extract this class into a separate git repository
23 // and use it as a submodule.
24 /**
25 * Gives access to meta info on this piece of software stored in the jar-file
26 * which provides this class.
27 * Start with version information.
28 */
29 public class MetaInfo {
30
31
32 /**
33 * Name of the folder <code>META-INF</code> in the jar file which provides this
34 * class.
35 * This contains
36 * <ul>
37 * <li>the folder <code>maven</code> created by maven and containing information
38 * like pom properties and pom itself which we currently do not consider</li>
39 * <li>the manifest file named {@link MetaInfo.ManifestInfo#MANIFEST_FILE}.</li>
40 * </ul>
41 */
42 private final static String META_FOLDER = "META-INF/";
43
44 /**
45 * Creates the stream for the file given by <code>fileName</code>.
46 *
47 * @param fileName
48 * a filename
49 * @return
50 * a non-null input stream to read from <code>fileName</code>.
51 * @throws BuildFailureException
52 * TMI01: if the stream to <code>fileName</code> could not be created.
53 */
54 static InputStream getStream(String fileName) throws BuildFailureException {
55 InputStream res = MetaInfo.class.getClassLoader().getResourceAsStream(fileName);
56 if (res == null) {
57 throw new BuildFailureException(
58 "TMI01: Cannot get stream to file '" + fileName + "'. ");
59 }
60 return res;
61 }
62
63 /**
64 * Returs properties read from the given file named <code>fileName</code>.
65 *
66 * @param fileName
67 * an input stream to read from <code>fileName</code>.
68 * @return
69 * Properties read from <code>fileName</code>.
70 * @throws BuildFailureException
71 * <ul>
72 * <li>TMI01: if the stream to <code>fileName</code> could not be created. </li>
73 * <li>TMI02: if the properties could not be read from <code>fileName</code>. </li>
74 * </ul>
75 */
76 static Properties getProperties(String fileName)
77 throws BuildFailureException {
78 try {
79 Properties properties = new Properties();
80 // may throw TMI01, also TFU01?
81 properties.load(MetaInfo.getStream(fileName));
82 return properties;
83 } catch (IOException e) {
84 // TBD: assign exception identifier
85 throw new BuildFailureException(
86 "TMI02: Cannot load properties from file '" + fileName + "'. ");
87 }
88 }
89
90
91
92 /**
93 * Reflects the pieces of information given by the manifest file
94 * with manifest version {@link #MANIFEST_VERSION} augmented
95 * by {@link #getCreatedBy()} and by {@link #getBuildJdkSpec()}
96 * which are both due to to the maven jar plugin and specific version
97 * given by {@link #MVN_JAR_PLUGIN}.
98 *
99 * TBD: change user and see whether author changes then also
100 * @author ernst
101 */
102 static class ManifestInfo {
103
104 /**
105 * The manifest version this class is designed for.
106 * When creating an instance, that version is checked
107 * and if the version found differs from that one, an exception is thrown.
108 */
109 private final static String MANIFEST_VERSION = "1.0";
110
111 /**
112 * An indicator that the jar file
113 * containing the manifest represented by this object
114 * was created by maven jar plugin with proper version.
115 */
116 // TBD: decide how to make independent of the version
117 private final static String MVN_JAR_PLUGIN = "Maven JAR Plugin 3.4.2";
118
119 /**
120 * The name of the manifest file which is in the folder {@link #META_FOLDER}
121 * of the jar file which provides this class.
122 */
123 private final static String MANIFEST_FILE = "MANIFEST.MF";
124
125 /**
126 * These attributes are added by the maven jar plugin in version 3.2.0 and is specific for that.
127 * Thus the according value is exactly the specification of that plugin with version info.
128 */
129 private final static Attributes.Name CREATED_BY =
130 new Attributes.Name("Created-By");
131
132 /**
133 * The version of the jdk used to build the jar containing the manifest
134 * represented by this object.
135 * These attributes are added by the maven jar plugin version 3.2.0 and is specific for that.
136 */
137 private final static Attributes.Name BUILD_JDK_SPEC =
138 new Attributes.Name("Build-Jdk-Spec");
139
140 // TBD: decide whether this is sensible
141 private final Manifest manifest;
142
143 /**
144 * The main attributes of the manifest.
145 */
146 private final Attributes mAtts;
147
148 /**
149 * Creates Manifest info instance.
150 *
151 * @throws BuildFailureException
152 * TMI01: if the stream to the manifest file is not readable
153 * TMI03: the manifest file is not readable although the stream is ok.
154 */
155 ManifestInfo() throws BuildFailureException {
156
157 //System.out.println("MANIFEST: ");
158 try {
159 // getStream may throw TMI01
160 this.manifest = new Manifest(getStream(META_FOLDER + MANIFEST_FILE));
161 } catch (IOException e) {
162 throw new BuildFailureException(
163 "TMI03: IOException reading manifest. ");
164 }
165 this.mAtts = this.manifest.getMainAttributes();
166 // check the manifest version
167 if (!MANIFEST_VERSION.equals(getManifestVersion())) {
168 throw new IllegalStateException(
169 "Found manifest with version '" + getManifestVersion()
170 + " where expected version " + MANIFEST_VERSION + ". ");
171 }
172 // CAUTION: don't use getCreatedBy() as this throws an exception
173 // if this jar is not created with the correct maven plugin
174 // (or even without maven at all)
175 if (!MVN_JAR_PLUGIN.equals(this.mAtts.get(CREATED_BY))) {
176 // MVN_JAR_PLUGIN includes the version also
177 throw new IllegalStateException(
178 "Found manifest not created by '" + MVN_JAR_PLUGIN + "' message was '"+this.mAtts.get(CREATED_BY) +"'. ");
179 }
180
181 // Map<String, Attributes> entriesMf = mf.getEntries();
182
183
184 // seems to correspond with Attributes.Name
185 // MANIFEST_VERSION,
186 // IMPLEMENTATION_TITLE, IMPLEMENTATION_VERSION, IMPLEMENTATION_VENDOR,
187 // SPECIFICATION_TITLE, SPECIFICATION_VERSION, SPECIFICATION_VENDOR,
188 //
189 // defined but not occurring:
190 // SIGNATURE_VERSION, CONTENT_TYPE, CLASS_PATH, MAIN_CLASS, SEALED,
191 // EXTENSION_LIST, EXTENSION_NAME, EXTENSION_INSTALLATION,
192 // IMPLEMENTATION_VENDOR_ID
193 // IMPLEMENTATION_URL,
194 //
195 // occurring but not defined in Attributes.Name:
196 // Created-By: Maven Jar Plugin 3.2.0, Build-Jdk-Spec: 11
197
198 //this.implVersion = this.mAtts.get(Attributes.Name.IMPLEMENTATION_VERSION).toString();
199
200 // TBC: seems to be empty in this case.
201 // in maven-jar-plugin set by manifestSections
202 // System.out.println("Manifest entries"+entriesMf);
203
204 // Enumeration entriesJar = mf.entries();
205 // System.out.println("jar entries"+entriesJar);
206 }
207
208 private String getAttrValue(Object name) {
209 // is in fact a string always but this is to detect null pointer exceptions
210 return this.mAtts.get(name).toString();
211 }
212
213 /**
214 * Returns the version of the implementation.
215 * This is the version given by the maven coordinates.
216 *
217 * @return
218 */
219 protected String getImplVersion() {
220 return getAttrValue(Attributes.Name.IMPLEMENTATION_VERSION);
221 }
222
223 /**
224 * Returns the vendor of the implementation.
225 * This is the vendor given by the maven <code>project.organization.name</code>.
226 *
227 * @return
228 */
229 protected String getImplVendor() {
230 return getAttrValue(Attributes.Name.IMPLEMENTATION_VENDOR);
231 }
232
233 protected String getManifestVersion() {
234 return getAttrValue(Attributes.Name.MANIFEST_VERSION);
235 }
236
237 protected String getSpecVersion() {
238 return getAttrValue(Attributes.Name.SPECIFICATION_VERSION);
239 }
240
241 // specific for Maven Jar Plugin 3.2.0 which is also the string returned.
242 protected String getCreatedBy() {
243 return getAttrValue(CREATED_BY);
244 }
245
246 // specific for Maven Jar Plugin 3.2.0
247 protected String getBuildJdkSpec() {
248 return getAttrValue(BUILD_JDK_SPEC);
249 }
250
251
252 protected String getPackageImplVersion() {
253 return this.getClass().getPackage().getImplementationVersion();
254 }
255
256 // TBD: tie names to values.
257 @Override
258 public String toString() {
259 StringBuilder stb = new StringBuilder();
260 for (String line : toStringArr()) {
261 stb.append(line);
262 stb.append('\n');
263 }
264 return stb.toString();
265 }
266
267 public String[] toStringArr() {
268 // List<String> lines = new ArrayList<String>();
269 // Object key, value;
270 // for (Map.Entry<Object,Object> entry : mAtts.entrySet()) {
271 // key = entry.getKey();
272 // value = entry.getValue();
273 // lines.add(" '" + key + "' cls: " + key.getClass() +
274 // "'->'" + value + "' cls: " + value.getClass());
275 // }
276 // return lines.toArray(new String[lines.size()]);
277
278 return new String[] {"MANIFEST: (" + getManifestVersion() + ")",
279 " Implementation-Version: '" + getImplVersion() + "'",
280 "PackageImplementation-Version: '" + getPackageImplVersion() + "'"
281 //"creator: '" + getCreatedBy() + "'",
282 //"version jdk: '" + getBuildJdkSpec() + "'"
283 };
284 }
285
286 } // class ManifestInfo
287
288
289 /**
290 * Reflects the pieces of information
291 * given by the <code>git-commit-id-plugin</code> in the current(?) version.
292 *
293 *
294 * @author ernst
295 */
296 class GitProperties {
297 private final static String GIT_PROPS_FILE = "git.properties";
298
299 private final static String GIT_BUILD_VERSION = "git.build.version";
300 private final static String GIT_COMMIT_ID_DESCRIBE =
301 "git.commit.id.describe";
302 private final static String GIT_CLOSEST_TAG_NAME = "git.closest.tag.name";
303 private final static String GIT_CLOSEST_TAG_COMMIT_COUNT =
304 "git.closest.tag.commit.count";
305 private final static String GIT_COMMIT_ID_ABBREV = "git.commit.id.abbrev";
306 private final static String GIT_DIRTY = "git.dirty";
307 private final static String GIT_BUILD_TIME = "git.build.time";
308
309
310 private final Properties properties;
311
312 /**
313 * @throws BuildFailureException
314 * <ul>
315 * <li>TMI01: if the stream to {@link #GIT_PROPS_FILE} could not be created. </li>
316 * <li>TMI02: if the properties could not be read from @link #GIT_PROPS_FILE}. </li>
317 * </ul>
318 */
319 GitProperties() throws BuildFailureException {
320 // TBD: extract properties
321 this.properties = getProperties(GIT_PROPS_FILE);
322
323 //String gitBuildVersion = getBuildVersion();
324 // latex-maven-plugin-1.4-44-g555bde8-dirty
325 // is constructed from
326 //String gitCommitIdDescribe = getCommitIdDescribe();
327 // also interesting:
328 }
329
330 private String getAttrValue(String key) {
331 // is in fact a string always but this is to detect null pointer exceptions
332 return this.properties.get(key).toString();
333 }
334
335 /**
336 * Returns the build version which is the same as the version in the coordinate
337 * provided the <code>maven-release-plugin</code> is used correctly.
338 * @return
339 * the build version.
340 */
341 String getBuildVersion() {
342 return getAttrValue(GIT_BUILD_VERSION);
343 }
344
345 // latex-maven-plugin-1.4-44-g555bde8-dirty
346 /**
347 * Returns the commit identifiers description TBC which consists of
348 * <ul>
349 * <li> the closest tag name as given by {@link #getClosestTagName()} </li>
350 * <li> the number of commits since the last tag
351 * given by {@link #getClosestTagCommitCount()}</li>
352 * <li> something unknown which leads to the 'g'</li>
353 * <li> git.commit.id.abbrev=555bde8</li>
354 * <li> 'dirty' if {@link #getDirty()} returns true. </li>
355 * </ul>
356 * each segment separated by a dash
357 *
358 * @return
359 * the commit identifiers description
360 */
361 String getCommitIdDescribe() {
362 return getAttrValue(GIT_COMMIT_ID_DESCRIBE);
363 }
364
365 // latex-maven-plugin-1.4
366 /**
367 * This is the artifact id and, separated by a dash,
368 * by the current version tag, if there is one,
369 * else, i.e. for snapshots, by the next lowest.
370 * The tag given is current, iff {@link #getClosestTagCommitCount()} returns 0.
371 *
372 * @return
373 * the closest tag name for the last commit.
374 */
375 String getClosestTagName() {
376 return getAttrValue(GIT_CLOSEST_TAG_NAME);
377 }
378
379 // TBD: shall be a number, not a string
380 /**
381 * Returns the number of commits since the last tag
382 * which is given by {@link #getClosestTagName()}.
383 *
384 * @return
385 * the number of commits since the last tag.
386 */
387 String getClosestTagCommitCount() {
388 return getAttrValue(GIT_CLOSEST_TAG_COMMIT_COUNT);
389 }
390
391 // TBC: maybe a string maybe an int or what.
392 /**
393 * Returns the abbreviated hash of the last commit.
394 *
395 * @return
396 * the abbreviated hash of the last commit.
397 */
398 String getCommitIdAbbrev() {
399 return getAttrValue(GIT_COMMIT_ID_ABBREV);
400 }
401
402 // TBD: this shall not be a string but a boolean
403 /**
404 * Returns whether the current working environment is dirty,
405 * i.e. there is something not committed inside.
406 *
407 * @return
408 * whether the current working environment is dirty.
409 */
410 String getDirty() {
411 return getAttrValue(GIT_DIRTY);
412 }
413
414 /**
415 * Returns the build time TBD: not just a string
416 * @return
417 * the build time as a string.
418 */
419 String getBuildTime() {
420 return getAttrValue(GIT_BUILD_TIME);
421 }
422
423 @Override
424 public String toString() {
425 StringBuilder stb = new StringBuilder();
426 stb.append("build version: '" + getBuildVersion() + "'\n");
427 stb.append("commit id desc: '" + getCommitIdDescribe() + "'\n");
428 stb.append("buildTime: '" + getBuildTime() + "'\n");
429 return stb.toString();
430 }
431
432 void log() {
433 MetaInfo.this.log.info("build version: '" + getBuildVersion() + "'");
434 MetaInfo.this.log.info("commit id desc: '" + getCommitIdDescribe() + "'");
435 MetaInfo.this.log.info("buildTime: '" + getBuildTime() + "'");
436 }
437
438 } // class GitProperties
439
440 static class Version implements Comparable<Version> {
441
442 private final static String VERSION_UNKNOWN = "<unknown>";
443
444 private final String text;
445
446 private final Matcher matcher;
447
448 /**
449 * The version as a string of the form as given by the converter
450 */
451 private final String versionStr;
452
453 /**
454 * The segments of the version string.
455 * Typically these are separated by a dot,
456 * but other separators or the lack of a separator may happen also.
457 */
458 private final List<Number> segments;
459
460 /**
461 * Create a version for a converter <code>conv</code>
462 * invoking it with the proper option using <code>executor</code>.
463 * This is used in {@link MetaInfo#printMetaInfo(boolean, SortedSet<Converter>)}
464 * to create the version info of a converter.
465 *
466 * @param conv
467 * a converter.
468 * @param executor
469 * an executor to execute the command of the converter
470 * to obtain a feedback containing the version.
471 * @throws BuildFailureException
472 * TEX01 if invocation of <code>command</code> fails very basically.
473 */
474 Version(Converter conv, CommandExecutor executor)
475 throws BuildFailureException {
476 this(conv.getVersionEnvironment(), conv.getVersionPattern(),
477 // may throw BuildFailureException
478 conv.getVersionInfo(executor));
479 }
480
481 /**
482 * Create a version from given pattern.
483 * This is used in {@link Version(Converter, CommandExecutor)}
484 * to create the version info of a converter
485 * but also in {@link VersionInterval#VersionInterval(Converter, String)}
486 * to get the minimum/maximum expected version of a converter.
487 *
488 * @param patternEnv
489 * The pattern <code>patternEnv</code> is a format string
490 * containing the literal <code>%s</code>.
491 * This literal indicates the location of the version pattern <code>patternVrs</code>.
492 * The string <code>text</code> shall match <code>patternEnv</code>
493 * with <code>%s</code> substituted by <code>patternVrs</code>.
494 * @param patternVrs
495 * the regex pattern expected for this version.
496 * This is in a form
497 * as returned by {@link Converter#getVersionPattern()}.
498 * @param text
499 * some text containing (among other things)
500 * version information described by <code>patternVrs</code>
501 * and conforming to <code>patternEnv</code>.
502 * The origin of that text may be output of invocation of a converter
503 * or a property file with allowed versions (as version intervals).
504 */
505 Version(String patternEnv, String patternVrs, String text) {
506 this.text = text;
507 this.matcher =
508 Pattern.compile(String.format(patternEnv, patternVrs)).matcher(text);
509 if (!this.matcher.find()) {
510 this.versionStr = VERSION_UNKNOWN;
511 this.segments = null;// TBD: eliminate
512 return;
513 }
514 this.versionStr = this.matcher.group(1);
515 this.segments = new ArrayList<Number>(this.matcher.groupCount()-1);
516 String segment;
517 Number num;
518 // group 1 is the enclosing group. Thus we start with the second one
519 for (int idx = 2; idx <= this.matcher.groupCount(); idx++) {
520 segment = this.matcher.group(idx);
521 if (segment == null) {
522 break;
523 }
524 num = segStr2Num(segment);
525 this.segments.add(num);
526 }
527 }
528
529 /**
530 * Returns a number representing the segment of a version string.
531 * This is a Byte if the segment is a lower case letter,
532 * an Integer if it is an integer and a Double if it has the form number.number
533 * @param segment
534 * @return
535 */
536 private static Number segStr2Num(String segment) {
537 if (segment.isEmpty()) {
538 // TBD: very weak.
539 // This is allowed only at last place with pattern [a-z]
540 return Byte.MAX_VALUE;
541 }
542 if (Pattern.matches("[a-z]", segment)) {
543 return Byte.valueOf((byte) segment.codePointAt(0));
544 }
545 // TBC: why works the 2nd version, but the 1st does not?
546 // num = segment.indexOf('.') == -1
547 // ? Integer.valueOf(segment)
548 // : Double .valueOf(segment);
549 if (segment.indexOf('.') == -1) {
550 return Integer.valueOf(segment);
551 } else {
552 return Double.valueOf(segment);
553 }
554 }
555
556 boolean isMatching() {
557 return this.segments != null;
558 }
559
560 String getText() {
561 return this.text;
562 }
563
564 String getString() {
565 return this.versionStr;
566 }
567
568 private List<Number> getSegments() {
569 return this.segments;
570 }
571
572 public int compareTo(Version other) {
573 int idxMin = Math.min(getSegments().size(), other.getSegments().size());
574 int sign;
575 for (int idx = 0; idx < idxMin; idx++) {
576 // TBD: eliminate hack
577 sign = (int) Math.signum(getSegments().get(idx).doubleValue()
578 - other.getSegments().get(idx).doubleValue());
579 switch (sign) {
580 case 0:
581 continue;
582 case -1:
583 // fall through
584 case +1:
585 return sign;
586 default:
587 throw new IllegalStateException("Found unexpected signum. ");
588 }
589 }
590 // TBD: unoconv is an example where 0.9 and 0.9.0 occur,
591 // in different distributions but both versions identical
592 // This must be taken into account
593 // For fig2dev with verions 3.2.7x and 3.2.8, this seems ok.
594 return getSegments().size() - other.getSegments().size();
595 }
596
597 } // class Version
598
599 /**
600 * An interval of versions, representing intervals
601 * between the boundaries {@link #min} and {@link #min}
602 * which offers a containment predicate {@link #contains(Version)}.
603 * <p>
604 * Tied to this is a string representation given by {@link #toString()}
605 * and also used by the constructor {@link #VersionInterval(Converter, String)}.
606 */
607 static class VersionInterval {
608
609 /**
610 * Separator between minimum version and maximum version in string representation.
611 * This must be a character not occurring within a version string.
612 * We have a piece of software, see {@link Converter#Latex2rtf}
613 * which has a blank in the version string and as a consequence,
614 * we cannot use a blank here.
615 */
616 private final static char SEP = ';';
617
618 /**
619 * The pattern for a one point interval, essentially the version within brackets.
620 * Note that the version string is represented as the formatter <code>%s</code>.
621 *
622 * @see #ENV_PATTERN2LOW
623 * @see #ENV_PATTERN2HIG
624 */
625 private final static String ENV_PATTERN1 = "^\\[%s\\]$";
626
627 /**
628 * The pattern for a non-degenerate interval,
629 * minimum and maximum version separated by {@link #SEP}
630 * and enclosed within brackets.
631 * The minimum version string is represented as the formatter <code>%s</code>
632 * whereas the maximum version string is represented by the general pattern.
633 *
634 * @see #ENV_PATTERN1
635 * @see #ENV_PATTERN2HIG
636 */
637 private final static String ENV_PATTERN2LOW = "^\\[%s" + SEP + ".*\\]$";
638
639 /**
640 * The pattern for a non-degenerate interval,
641 * minimum and maximum version separated by {@link #SEP}
642 * and enclosed within brackets.
643 * The maximum version string is represented as the formatter <code>%s</code>
644 * whereas the minimum version string is represented by the general pattern.
645 *
646 * @see #ENV_PATTERN1
647 * @see #ENV_PATTERN2LOW
648 */
649 private final static String ENV_PATTERN2HIG = "^\\[.*" + SEP + "%s\\]$";
650
651 /**
652 * The minimum version (inclusive) contained in this interval.
653 */
654 private final Version min;
655
656 /**
657 * The maximum version (inclusive) contained in this interval.
658 */
659 private final Version max;
660
661 /**
662 * Creates a version interval from the converter and from the text string
663 * read from the version properties file.
664 *
665 * @param conv
666 * the converter for which to create the version interval.
667 * This is used mostly to determine {@link Converter#getVersionPattern()}.
668 * @param text
669 * the text representation for the interval to be created
670 * which is as described for {@link #toString()}.
671 * This may well be <code>null</code>.
672 * @throws IllegalStateException
673 * if either <code>text</code> is <code>null</code>
674 * which means that there is no version for <code>conv</code>
675 * or if this created version interval
676 * does not have the representation given by <code>text</code>.
677 */
678 VersionInterval(Converter conv, String text) {
679 String patternVrs = conv.getVersionPattern();
680 if (text == null) {
681 throw new IllegalStateException(
682 "Found no expected version for converter " + conv + ". ");
683 }
684 if (text.indexOf(SEP) == -1) {
685 // Here, we have an interval [element]
686 this.min = new Version(ENV_PATTERN1, patternVrs, text);
687 this.max = this.min;
688 if (!this.min.isMatching()) {
689 throw new IllegalStateException(String.format(
690 "Expected version interval '%s' "
691 + "does not match expression '^\\[%s\\]$'. ",
692 text, patternVrs));
693 }
694 } else {
695 // Here, we have an interval [min;max]
696 this.min = new Version(ENV_PATTERN2LOW, patternVrs, text);
697 this.max = new Version(ENV_PATTERN2HIG, patternVrs, text);
698 if (!(this.min.isMatching() && this.max.isMatching())) {
699 throw new IllegalStateException(String.format(
700 "Expected version interval '%s' "
701 + "does not match expression '^\\[%s%c%s\\]$'. ",
702 text, patternVrs, SEP, patternVrs));
703 }
704 }
705 String expVersionItvStr = toString();
706 if (!text.equals(expVersionItvStr)) {
707 throw new IllegalStateException(
708 String.format("Expected version '%s' reconstructed as '%s'. ", text,
709 expVersionItvStr));
710 }
711 }
712
713 /**
714 * Returns whether <code>version</code> is contained in this interval.
715 *
716 * @param version
717 * some version.
718 * @return
719 * whether <code>version</code> is contained in this interval.
720 */
721 boolean contains(Version version) {
722 return this.min.compareTo(version) <= 0
723 && this.max.compareTo(version) >= 0;
724 }
725
726 /**
727 * Returns a representation inspired by math and used in version property file.
728 * It is just the format
729 * which is read by the constructor {@link #VersionInterval(Converter, String)}.
730 *
731 * @return
732 * string representation of the form <code>[min;max]</code>
733 * except if <code>min</code> and <code>max</code> coincide,
734 * then it is just <code>[min]</code>.
735 */
736 @Override
737 public String toString() {
738 StringBuilder res = new StringBuilder();
739 res.append('[');
740 res.append(this.min.getString());
741
742 if (this.min != this.max) {
743 res.append(SEP);
744 res.append(this.max.getString());
745 }
746 res.append(']');
747 return res.toString();
748 }
749 } // class VersionInterval
750
751 /**
752 * Represents the maven coordinates of this maven plugin.
753 */
754 static class Coordinates {
755 final String groupId;
756 final String artifactId;
757 final String version;
758
759 Coordinates(String groupId, String artifactId, String version) {
760 this.groupId = groupId;
761 this.artifactId = artifactId;
762 this.version = version;
763 }
764 } // class Coordinates
765
766 /**
767 * The name of the file containing the version properties.
768 * For each {@link Converter}, the range of possible versions is given
769 * in a separate line of the form <code>command=[a;b]</code>
770 * or <code>command=[a]</code> for a=b.
771 * Also comments starting with {@literal #} are allowed.
772 */
773 private final static String VERSION_PROPS_FILE = "version.properties";
774
775 /**
776 * Format string used in {@link #printMetaInfo(boolean, SortedSet<Converter>)}
777 * to define a table of converters and their versions
778 * with rows (warning), converter, version quote,
779 * actual version and allowed version interval.
780 */
781 private final static String TOOL_VERSION_FORMAT = "%s%-18s %s '%s'%s%s";
782
783
784
785 /**
786 * The command <code>which</code> as a string.
787 */
788 private static final String CMD_WHICH = "which";
789
790 private static final String COORDINATE_PROPERTY_FILE_NAME =
791 META_FOLDER + "maven/" + "eu.simuline.m2latex/"
792 + "latex-maven-plugin/" + "pom.properties";
793
794 /**
795 * Executor to find the version string.
796 */
797 private final CommandExecutor executor;
798
799 /**
800 * Logs information on versions.
801 * Typically, just info are logged,
802 * but if a version could not be read or if a version is not as expected,
803 * also a warning is logged.
804 */
805 private final LogWrapper log;
806
807 /**
808 * Creates meta info using an executor to find out the version
809 * and a logger to log warnings and infos, e.g. if a version does not fit the expectations.
810 *
811 * @param executor
812 * the executor to execute the converters to find out their version.
813 * @param log
814 * the logger to log info on versions.
815 */
816 MetaInfo(CommandExecutor executor, LogWrapper log) {
817 this.executor = executor;
818 this.log = log;
819 }
820
821 // TBD: no general properties but class Coordinates
822 // This allows access without specifying keys as strings
823 /**
824 * Returns the coordinates of this maven plugin as a properties.
825 *
826 * @return
827 * The coordinates of this plugin with fields
828 * 'groupId', 'artifactId', 'version'.
829 * @throws BuildFailureException
830 * TMI01 if the stream to the according properties file could not be created
831 * TMI02 if the properties could not be read.
832 */
833 Coordinates getCoordinates() throws BuildFailureException {
834 // may throw TMI01
835 Properties properties = getProperties(COORDINATE_PROPERTY_FILE_NAME);
836 assert "[groupId, artifactId, version]".equals(properties
837 .stringPropertyNames().toString()) : "Found unexpected properties ";
838 return new Coordinates(properties.getProperty("groupId"),
839 properties.getProperty("artifactId"),
840 properties.getProperty("version"));
841 }
842
843 /**
844 * The string 'version' to be inserted in a warning
845 * that the version of a converter is not in the expected range.
846 * TBD: more precise information.
847 * Is invoked in {@link #versionLine(String, String, boolean, String, String, String)}
848 * but only in the context where this is needed for clarification.
849 */
850 private final static String VERSION_QUOTE = "version ";
851
852 // CAUTION, depends on the maven-jar-plugin and its version
853 /**
854 * Prints meta information, mainly version information
855 * on this software and on the converters and checker tools used.
856 * <p>
857 * WMI01: If the version string of a converter cannot be read.
858 * WMI02: If the version of a converter is not as expected.
859 *
860 * @param includeVersionInfo
861 * whether to include plain version info; else warnings only.
862 * @param convertersExcluded
863 * set of excluded converters.
864 * @return
865 * whether a warning has been issued.
866 * @throws BuildFailureException
867 * <ul>
868 * <li>TMI01: if the stream to either the manifest file
869 * or to a property file, either {@LINK #VERSION_PROPS_FILE}
870 * or {@link MetaInfo.GitProperties#GIT_PROPS_FILE}
871 * could not be created. </li>
872 * <li>TMI02: if the properties could not be read
873 * from one of the two property files mentioned above. </li>
874 * <li>TMI03: the manifest file is not readable although the stream is ok. </li>
875 * <li>TSS05: if converters are excluded in the pom which are not known. </li>
876 * </ul>
877 */
878 public boolean printMetaInfo(final boolean includeVersionInfo,
879 SortedSet<Converter> convertersExcluded)
880 throws BuildFailureException {
881
882 if (includeVersionInfo) {
883 // may throw TMI01, TMI03
884 ManifestInfo manifestInfo = new ManifestInfo();
885 // TBC: how does maven determine that version??
886 //String versionMf = manifestInfo.getImplVersion();
887 //System.out.println("mf version: '" + versionMf + "'");
888 this.log.info("Manifest properties: ");
889 for (String line : manifestInfo.toStringArr()) {
890 this.log.info(line);
891 }
892
893 //String mavenDir = META_FOLDER + "maven/";
894 //URL url = MetaInfo.class.getClassLoader().getResource(mavenDir);
895 // try {
896 // //System.out.println("path: "+url);
897 // //System.out.println("path: "+url.toURI());
898 // //File[] files = new File(url.toURI()).listFiles();
899 // //System.out.println("cd maven; ls: "+java.util.Arrays.asList(files));
900 // } catch (URISyntaxException e) {
901 // throw new IllegalStateException("Found unexpected type of url: "+url);
902 // }
903 //String propertyFileName = META_FOLDER + "maven/" + "eu.simuline.m2latex/"
904 // + "latex-maven-plugin/" + "pom.properties";
905 this.log.info("pom properties: coordinates");
906 // may throw TMI01, TMI02
907 Coordinates coords = getCoordinates();
908 this.log.info("groupId: '" + coords.groupId + "'");
909 this.log.info("artifactId: '" + coords.artifactId + "'");
910 this.log.info("version: '" + coords.version + "'");
911
912 // propertyFileName = "META-INF/"
913 // + "maven/"
914 // + "eu.simuline.m2latex/"
915 // + "latex-maven-plugin/"
916 // + "pom.xml";
917 // url = this.getClass().getClassLoader()
918 // .getResource(propertyFileName);
919 // System.out.println("url:"+url);
920 // MavenXpp3Reader reader = new MavenXpp3Reader();
921 // Model model = reader.read(new InputStreamReader(url.openStream()));
922
923 GitProperties gitProperties = new GitProperties();
924 this.log.info("git properties: ");
925 gitProperties.log();
926 String gitBuildVersion = gitProperties.getBuildVersion();
927 //System.out.println("git.build.version: " + gitBuildVersion);
928 assert gitBuildVersion.equals(manifestInfo.getImplVersion());
929
930 // String gitCommitIdDescribe = gitProperties.getCommitIdDescribe();
931 // System.out.println("git.commit.id.describe: " + gitCommitIdDescribe);
932 // String gitBuildTime = gitProperties.getBuildTime();
933 // System.out.println("git.build.time: " + gitBuildTime);
934
935 // TBD: rework; this is just the beginning
936 // System.out.println("version properties of converters: ");
937 // Properties properties = getProperties("version.properties");
938 //
939 // System.out.println("version.makeindex:"
940 // +properties.getProperty("version.makeindex"));
941
942 // headlines
943 this.log.info("tool versions: ");
944 // Note that
945 // "?warning? " in the headline lines up with
946 // " " the placeholder if no warning occurs.
947 // Both are preceeded by "[INFO] "
948 // Also,
949 // " W"
950 this.log.info(versionLine("?warning? ", "command",
951 includeVersionInfo, "actual version", "(not)in",
952 "[expected version interval]"));
953 }
954
955 Properties versionProperties = getProperties(VERSION_PROPS_FILE);
956 if (versionProperties.size() > Converter.values().length) {
957 // Relation < need not be checked since all converters are checked below
958 throw new IllegalStateException("Number of version properties "
959 + versionProperties.size() + " does not fit number of converters "
960 + Converter.values().length + ". ");
961 }
962
963 boolean doWarnAny = false;
964 // may throw BuildFailureException TSS05
965 // SortedSet<Converter> convertersExcluded =
966 // this.settings.getConvertersExcluded();
967 // collects converters not found but also not excluded.
968 SortedSet<Converter> convertersNotFound = new TreeSet<Converter>();
969 // TBD: try to deal with makeindex using stdin instead of dummy file:
970 // InputStream sysInBackup = System.in;
971 for (Converter conv : Converter.values()) {
972 doWarnAny |= logConverterInfo(conv, includeVersionInfo, convertersExcluded, convertersNotFound, versionProperties);
973 } // for
974
975 if (includeVersionInfo) {
976 // keep informed about excluded tools
977 if (!convertersExcluded.isEmpty()) {
978 this.log.info("tools excluded: ");
979 this.log.info(Converter.toCommandsString(convertersExcluded));
980 }
981
982 // keep informed about included tools not found
983 if (!convertersNotFound.isEmpty()) {
984 this.log.info("tools not found: ");
985 this.log.info(Converter.toCommandsString(convertersNotFound));
986 }
987 }
988 return doWarnAny;
989 }
990
991 /**
992 * Returns a so called version line describing the version of a converter named {@code cmdStr}
993 * if invoked by {@link #logConverterInfo(Converter, boolean, SortedSet, SortedSet, Properties)}.
994 * If {@code includeVersionInfo}, then the version lines form a table
995 * and this method is also used to create the header of this table
996 * which is the case if invoked by {@link #printMetaInfo(boolean, SortedSet)} directly.
997 *
998 * Except {@code includeVersionInfo} the parameters describe the columns of the table
999 * and so it is clear that if used to create the header, the values are quite special.
1000
1001 * It consists of
1002 * a <code>warnStr</code> which may be a warning specifier or empty, either because it is not a warning
1003 * or because the warning was displayed before (in case the version cannot be detected)
1004 * @param warnStr
1005 * the warning string or the header of the warning column
1006 * The warning string is either {@code 'WMI02: '} indicating that the version of {@code cmdStr} does not fit the expectation,
1007 * or an empty string of appropriate length indicating that the version fits (in an {@code [INFO]} line),
1008 * or an empty string if the version cannot be determined.
1009 * In the latter case, warning {@code WMI01} is displayed in the line before clarifying the problem.
1010 * Thus no further warning indication is needed and the {@code warnStr} is empty.
1011 * On the other hand the line starts with {@code [WARNING]} so the empty {@code warnStr} is shorter than for an {@code [INFO]} line.
1012 * @param cmdStr
1013 * the name of the converter under consideration or the header of the command column.
1014 * @param includeVersionInfo
1015 * an indication whether this line is among lines displaying info also which means it is in a table,
1016 * possibly the header of that table.
1017 * Else for clarification of the context {@link #VERSION_QUOTE} is included.
1018 * @param versionStr
1019 * the version string of {@code cmdStr} or the header of the according column.
1020 * If the version of {@code cmdStr} cannot be determined, it is given by {@link MetaInfo.Version#VERSION_UNKNOWN}.
1021 * @param inclStr
1022 * the string {@code not?in} or in the context of the header,
1023 * {@code in} if the version of {@code cmdStr} is known to be in range,
1024 * {@code not in} if the version of {@code cmdStr} is known to be not in range,
1025 * {@code not?in} if it is unknown whether the version of {@code cmdStr} is in range,
1026 * @param expVersionInterval
1027 * the interval of allowed versions of {@code cmdStr} or in the context of the header.
1028 * @return
1029 * the version line of a converter {@code cmdStr} or the header of the table of version lines.
1030 */
1031 private static String versionLine(String warnStr, String cmdStr, boolean includeVersionInfo,
1032 String versionStr, String inclStr, String expVersionInterval) {
1033 return String.format(TOOL_VERSION_FORMAT, warnStr, cmdStr+":",
1034 includeVersionInfo ? "" : VERSION_QUOTE, versionStr, inclStr, expVersionInterval);
1035 }
1036
1037 /**
1038 * Logs info or warning on the given converter
1039 * and returns whether a warning was emitted; else it was an info line or nothing at all.
1040 *
1041 * @param conv
1042 * The converter under consideration.
1043 * @param includeVersionInfo
1044 * whether to include plain version info; else warnings only.
1045 * @param convertersExcluded
1046 * set of excluded converters.
1047 * @param convertersNotFound
1048 * to collect the set of converters not found.
1049 * This shall be empty when invoking this method.
1050 * @param versionProperties
1051 * The properties describing the allowed version intervals
1052 * for all registered converters.
1053 * @return
1054 * whether a warning has been logged.
1055 * @throws BuildFailureException
1056 * TBD
1057 */
1058 private boolean logConverterInfo(Converter conv,
1059 boolean includeVersionInfo,
1060 SortedSet<Converter> convertersExcluded,
1061 SortedSet<Converter> convertersNotFound,
1062 Properties versionProperties) throws BuildFailureException {
1063
1064 assert(convertersNotFound.isEmpty());
1065 if (convertersExcluded.contains(conv)) {
1066 // Note that for excluded converters, no warnings are emitted.
1067 return false;
1068 }
1069
1070 // System.setIn(new ByteArrayInputStream("\u0004\n".getBytes()));
1071 String cmdStr = conv.getCommand();
1072
1073 CmdResult resultWhich = this.executor.executeEmptyEnv(TexFileUtils.getEmptyIdx().getParentFile(),
1074 null,
1075 CMD_WHICH,
1076 CommandExecutor.ReturnCodeChecker.Never,
1077 new String[] { cmdStr });
1078 if (resultWhich.returnCode == 1) {
1079 // skip if command cmd is unknown to command which.
1080 // Note that converters which are not accessible (typically not installed)
1081 // do not cause warnings here, because when using them, the situation is pretty
1082 // clear.
1083 // This is different for unexpected behavior caused by version not taken into
1084 // account.
1085 // Nevertheless, the converters not found are listed as an information,
1086 // as the excluded are.
1087 convertersNotFound.add(conv);
1088 return false;
1089 }
1090
1091 // get actual version of the converter and expected version interval
1092 Version actVersionObj = new Version(conv, this.executor);
1093 String expVersionStr = versionProperties.getProperty(cmdStr);
1094 VersionInterval expVersionInterval = new VersionInterval(conv, expVersionStr);
1095
1096 String warnStr, inclStr;
1097 boolean doWarn = false;
1098 if (actVersionObj.isMatching()) {
1099 // Here, we can find out,
1100 // whether the version of the converter fits the interval
1101 doWarn = !expVersionInterval.contains(actVersionObj);
1102 if (doWarn) {
1103 inclStr = "not in";
1104 warnStr = "WMI02: ";
1105 } else {
1106 // Here is the only branch, where no warning is logged.
1107 if (!includeVersionInfo) {
1108 return false;
1109 }
1110 // Here, an INFO must be logged.
1111 // "[WARNING] WMIxx: " and
1112 // "[INFO] " must line up.
1113 warnStr = " ";
1114 // warnStr is the sting after "[INFO] "
1115 inclStr = "in";
1116 }
1117 } else {
1118 // Here, we cannot find out,
1119 // whether the version of the converter fits the interval
1120 doWarn = true;
1121 this.log.warn("WMI01: Version string from converter " + conv
1122 + " did not match expected form: \n" + actVersionObj.getText());
1123 inclStr = "not?in";
1124 // no warning number, still the above is valid
1125 // and so this line is a warning (doWarn=true)
1126 // The warn string must line up with
1127 // "[WARNING] WMI01: ", means have the same length as "WMI01: "
1128 warnStr = " ";
1129 }
1130
1131 String logMsg = versionLine(warnStr, cmdStr, includeVersionInfo,
1132 actVersionObj.getString(), inclStr, expVersionStr);
1133 // this.log.info("actVersion: "+actVersionObj.getSegments());
1134 // this.log.info("expVersion: "+expVersionObj.getSegments());
1135 // this.log.info("actVersion: "+actVersionObj.getSegmentsAsStrings());
1136 // this.log.info("expVersion: "+expVersionObj.getSegmentsAsStrings());
1137 // this.log.info("actVersion?expVersion:
1138 // "+actVersionObj.compareTo(expVersionObj));
1139 if (doWarn) {
1140 this.log.warn(logMsg);
1141 } else {
1142 this.log.info(logMsg);
1143 }
1144 return doWarn;
1145 } // treatConverter
1146
1147 }