View Javadoc
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 }