1 /*
2  * This file is part of ELKI:
3  * Environment for Developing KDD-Applications Supported by Index-Structures
4  *
5  * Copyright (C) 2018
6  * ELKI Development Team
7  *
8  * This program is free software: you can redistribute it and/or modify
9  * it under the terms of the GNU Affero General Public License as published by
10  * the Free Software Foundation, either version 3 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16  * GNU Affero General Public License for more details.
17  *
18  * You should have received a copy of the GNU Affero General Public License
19  * along with this program. If not, see <http://www.gnu.org/licenses/>.
20  */
21 package de.lmu.ifi.dbs.elki.application.internal;
22 
23 import java.io.BufferedOutputStream;
24 import java.io.File;
25 import java.io.FileOutputStream;
26 import java.io.IOException;
27 import java.io.OutputStream;
28 import java.lang.reflect.Field;
29 import java.lang.reflect.Method;
30 import java.util.ArrayList;
31 import java.util.Collections;
32 import java.util.Comparator;
33 import java.util.HashMap;
34 import java.util.HashSet;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.TreeSet;
38 
39 import javax.xml.parsers.DocumentBuilder;
40 import javax.xml.parsers.DocumentBuilderFactory;
41 import javax.xml.parsers.ParserConfigurationException;
42 
43 import org.w3c.dom.DOMImplementation;
44 import org.w3c.dom.Document;
45 import org.w3c.dom.Element;
46 
47 import de.lmu.ifi.dbs.elki.logging.Logging;
48 import de.lmu.ifi.dbs.elki.utilities.ELKIServiceRegistry;
49 import de.lmu.ifi.dbs.elki.utilities.documentation.Reference;
50 import de.lmu.ifi.dbs.elki.utilities.xml.HTMLUtil;
51 
52 /**
53  * Build a reference documentation for all available parameters.
54  *
55  * @author Erich Schubert
56  * @since 0.3
57  *
58  * @assoc - - - Reference
59  */
60 public class DocumentReferences {
61   private static final String DOIPREFIX = "https://doi.org/";
62 
63   private static final String DBLPPREFIX = "DBLP:";
64 
65   private static final String DBLPURL = "https://dblp.uni-trier.de/rec/bibtex/";
66 
67   /**
68    * Logger
69    */
70   private static final Logging LOG = Logging.getLogger(DocumentReferences.class);
71 
72   /**
73    * @param args Command line arguments
74    */
main(String[] args)75   public static void main(String[] args) {
76     if(args.length < 1 || args.length > 2) {
77       LOG.warning("I need exactly one or two file names to operate!");
78       System.exit(1);
79     }
80     if(!args[0].endsWith(".html") || (args.length > 1 && !args[1].endsWith(".md"))) {
81       LOG.warning("File name doesn't end in expected extension!");
82       System.exit(1);
83     }
84 
85     List<Map.Entry<Reference, TreeSet<Object>>> refs = sortedReferences();
86     File references = new File(args[0]);
87     try (FileOutputStream reffo = new FileOutputStream(references); //
88         OutputStream refstream = new BufferedOutputStream(reffo)) {
89       documentReferences(refs, new HTMLFormat()).writeTo(refstream);
90     }
91     catch(IOException e) {
92       LOG.exception("IO Exception writing HTML output.", e);
93       System.exit(1);
94     }
95     if(args.length > 1) {
96       File refwiki = new File(args[1]);
97       try (FileOutputStream reffow = new FileOutputStream(refwiki); //
98           MarkdownDocStream refstreamW = new MarkdownDocStream(reffow)) {
99         documentReferences(refs, new MarkdownFormat(refstreamW));
100       }
101       catch(IOException e) {
102         LOG.exception("IO Exception writing Wiki output.", e);
103         System.exit(1);
104       }
105     }
106   }
107 
108   /**
109    * Abstract interface for output formats.
110    *
111    * @author Erich Schubert
112    *
113    * @param <T> Entry type
114    */
115   private interface Format<T> {
newEntry()116     T newEntry();
117 
init(String title)118     void init(String title);
119 
writeClass(T classdt, Class<?> cls)120     void writeClass(T classdt, Class<?> cls);
121 
writePackage(T classdt, Package pkg)122     void writePackage(T classdt, Package pkg);
123 
writeReference(Reference ref)124     void writeReference(Reference ref);
125 
linkFor(Class<?> cls)126     default String linkFor(Class<?> cls) {
127       return cls.getName().replace('.', '/') + ".html";
128     }
129 
linkFor(Package name)130     default String linkFor(Package name) {
131       return name.getName().replace('.', '/') + "/package-summary.html";
132     }
133   }
134 
135   /**
136    * HTML output format.
137    *
138    * @author Erich Schubert
139    */
140   private static class HTMLFormat implements Format<Element> {
141     private static final String CSSFILE = "stylesheet.css";
142 
143     private static final String MODIFICATION_WARNING = "WARNING: THIS DOCUMENT IS AUTOMATICALLY GENERATED. MODIFICATIONS MAY GET LOST.";
144 
145     Document htmldoc;
146 
147     Element maindl;
148 
HTMLFormat()149     HTMLFormat() throws IOException {
150       DocumentBuilder builder;
151       try {
152         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
153         builder = factory.newDocumentBuilder();
154       }
155       catch(ParserConfigurationException e) {
156         throw new IOException(e);
157       }
158       DOMImplementation impl = builder.getDOMImplementation();
159       htmldoc = impl.createDocument(HTMLUtil.HTML_NAMESPACE, HTMLUtil.HTML_HTML_TAG, null);
160     }
161 
162     @Override
init(String title)163     public void init(String title) {
164       assert !htmldoc.getDocumentElement().hasChildNodes();
165       // head
166       Element head = htmldoc.createElement(HTMLUtil.HTML_HEAD_TAG);
167       head.appendChild(htmldoc.createComment(MODIFICATION_WARNING));
168       htmldoc.getDocumentElement().appendChild(head);
169       // meta with charset information
170       Element meta = htmldoc.createElement(HTMLUtil.HTML_META_TAG);
171       meta.setAttribute(HTMLUtil.HTML_HTTP_EQUIV_ATTRIBUTE, HTMLUtil.HTML_HTTP_EQUIV_CONTENT_TYPE);
172       meta.setAttribute(HTMLUtil.HTML_CONTENT_ATTRIBUTE, HTMLUtil.CONTENT_TYPE_HTML_UTF8);
173       head.appendChild(meta);
174       // stylesheet
175       Element css = htmldoc.createElement(HTMLUtil.HTML_LINK_TAG);
176       css.setAttribute(HTMLUtil.HTML_REL_ATTRIBUTE, HTMLUtil.HTML_REL_STYLESHEET);
177       css.setAttribute(HTMLUtil.HTML_TYPE_ATTRIBUTE, HTMLUtil.CONTENT_TYPE_CSS);
178       css.setAttribute(HTMLUtil.HTML_HREF_ATTRIBUTE, CSSFILE);
179       head.appendChild(css);
180       // title
181       head.appendChild(htmldoc.createElement(HTMLUtil.HTML_TITLE_TAG)).setTextContent(title);
182       // body
183       Element body = htmldoc.createElement(HTMLUtil.HTML_BODY_TAG);
184       htmldoc.getDocumentElement().appendChild(body)//
185           .appendChild(htmldoc.createComment(MODIFICATION_WARNING));
186       body.appendChild(htmldoc.createElement(HTMLUtil.HTML_H1_TAG)).setTextContent(title + ":");
187       // Main definition list
188       maindl = htmldoc.createElement(HTMLUtil.HTML_DL_TAG);
189       body.appendChild(maindl);
190     }
191 
192     @Override
newEntry()193     public Element newEntry() {
194       // DT = definition term
195       Element classdt = htmldoc.createElement(HTMLUtil.HTML_DT_TAG);
196       maindl.appendChild(classdt);
197       return classdt;
198     }
199 
200     @Override
writeClass(Element classdt, Class<?> cls)201     public void writeClass(Element classdt, Class<?> cls) {
202       if(classdt.hasChildNodes()) {
203         classdt.appendChild(htmldoc.createTextNode(", "));
204       }
205       Element classa = htmldoc.createElement(HTMLUtil.HTML_A_TAG);
206       classa.setAttribute(HTMLUtil.HTML_ID_ATTRIBUTE, cls.getName());
207       classa.setAttribute(HTMLUtil.HTML_HREF_ATTRIBUTE, linkFor(cls));
208       classa.setTextContent(cls.getName());
209       classdt.appendChild(classa);
210     }
211 
212     @Override
writePackage(Element classdt, Package pkg)213     public void writePackage(Element classdt, Package pkg) {
214       if(classdt.hasChildNodes()) {
215         classdt.appendChild(htmldoc.createTextNode(", "));
216       }
217       Element classa = htmldoc.createElement(HTMLUtil.HTML_A_TAG);
218       classa.setAttribute(HTMLUtil.HTML_ID_ATTRIBUTE, pkg.getName());
219       classa.setAttribute(HTMLUtil.HTML_HREF_ATTRIBUTE, linkFor(pkg));
220       classa.setTextContent(pkg.getName());
221       classdt.appendChild(classa);
222     }
223 
224     @Override
writeReference(Reference ref)225     public void writeReference(Reference ref) {
226       // DD = definition description
227       Element classdd = htmldoc.createElement(HTMLUtil.HTML_DD_TAG);
228       maindl.appendChild(classdd);
229 
230       // Prefix
231       if(!ref.prefix().isEmpty()) {
232         Element prediv = htmldoc.createElement(HTMLUtil.HTML_DIV_TAG);
233         prediv.setTextContent(ref.prefix());
234         classdd.appendChild(prediv);
235       }
236       // Authors
237       Element authorsdiv = htmldoc.createElement(HTMLUtil.HTML_DIV_TAG);
238       authorsdiv.setTextContent(ref.authors());
239       classdd.appendChild(authorsdiv);
240       // Title
241       Element titlediv = htmldoc.createElement(HTMLUtil.HTML_DIV_TAG);
242       Element titleb = htmldoc.createElement(HTMLUtil.HTML_B_TAG);
243       titleb.setTextContent(ref.title());
244       titlediv.appendChild(titleb);
245       classdd.appendChild(titlediv);
246       // Booktitle
247       if(!ref.booktitle().isEmpty()) {
248         Element booktitlediv = htmldoc.createElement(HTMLUtil.HTML_DIV_TAG);
249         booktitlediv.setTextContent("In: " + ref.booktitle());
250         if(ref.booktitle().startsWith("Online:")) {
251           booktitlediv.setTextContent(ref.booktitle());
252         }
253         classdd.appendChild(booktitlediv);
254       }
255       // URL
256       if(!ref.url().isEmpty()) {
257         Element urldiv = htmldoc.createElement(HTMLUtil.HTML_DIV_TAG);
258         Element urla = htmldoc.createElement(HTMLUtil.HTML_A_TAG);
259         urla.setAttribute(HTMLUtil.HTML_HREF_ATTRIBUTE, ref.url());
260         urla.setTextContent(ref.url());
261         urldiv.appendChild(urla);
262         classdd.appendChild(urldiv);
263       }
264       // Bibkey
265       if(!ref.bibkey().isEmpty()) {
266         if(ref.bibkey().startsWith(DBLPPREFIX)) {
267           Element urldiv = htmldoc.createElement(HTMLUtil.HTML_DIV_TAG);
268           Element urla = htmldoc.createElement(HTMLUtil.HTML_A_TAG);
269           urla.setAttribute(HTMLUtil.HTML_HREF_ATTRIBUTE, DBLPURL + ref.bibkey());
270           urla.setTextContent(ref.url());
271           urldiv.appendChild(urla);
272           classdd.appendChild(urldiv);
273         }
274         else {
275           classdd.appendChild(htmldoc.createComment(ref.bibkey()));
276         }
277       }
278     }
279 
writeTo(OutputStream refstream)280     public void writeTo(OutputStream refstream) throws IOException {
281       HTMLUtil.writeXHTML(htmldoc, refstream);
282     }
283   }
284 
285   /**
286    * Markdown output format.
287    *
288    * @author Erich Schubert
289    */
290   private static class MarkdownFormat implements Format<Void> {
291     MarkdownDocStream out;
292 
293     boolean firstInEntry = false;
294 
MarkdownFormat(MarkdownDocStream out)295     public MarkdownFormat(MarkdownDocStream out) {
296       this.out = out;
297     }
298 
299     @Override
init(String title)300     public void init(String title) {
301       out.append("# ").append(title).par();
302     }
303 
304     @Override
newEntry()305     public Void newEntry() {
306       firstInEntry = true;
307       return null;
308     }
309 
310     @Override
writeClass(Void classdt, Class<?> cls)311     public void writeClass(Void classdt, Class<?> cls) {
312       if(!firstInEntry) {
313         out.append(',').lf();
314       }
315       out.append('[').append(cls.getName()).append("](./releases/current/doc/") //
316           .append(linkFor(cls)).append(')');
317       firstInEntry = false;
318     }
319 
320     @Override
writePackage(Void classdt, Package pkg)321     public void writePackage(Void classdt, Package pkg) {
322       if(!firstInEntry) {
323         out.append(',').lf();
324       }
325       out.append('[').append(pkg.getName()).append("](./releases/current/doc/") //
326           .append(linkFor(pkg)).append(')');
327       firstInEntry = false;
328     }
329 
330     @Override
writeReference(Reference ref)331     public void writeReference(Reference ref) {
332       out.lf();
333       // Prefix
334       if(!ref.prefix().isEmpty()) {
335         out.escaped(ref.prefix()).lf();
336       }
337       // Authors
338       out //
339           // authors
340           .append(ref.authors()).lf() //
341           // Title
342           .append("**").escaped(ref.title()).append("**").lf();
343       // Booktitle
344       if(!ref.booktitle().isEmpty() && !ref.booktitle().equals(ref.url()) && !ref.booktitle().equals("Online")) {
345         out.append(ref.booktitle().startsWith("Online:") ? "" : "In: ").escaped(ref.booktitle()).lf();
346       }
347       // URL
348       if(!ref.url().isEmpty()) {
349         if(ref.url().startsWith(DOIPREFIX)) {
350           out.append("[DOI:").append(ref.url(), DOIPREFIX.length(), ref.url().length())//
351               .append("](").append(ref.url()).append(')').lf();
352         }
353         else {
354           out.append("Online: <").append(ref.url()).append('>').lf();
355         }
356       }
357       // Bibkey, if we can link to DBLP:
358       if(!ref.bibkey().isEmpty()) {
359         if(ref.bibkey().startsWith(DBLPPREFIX)) {
360           out.append("[DBLP:").append(ref.bibkey(), DBLPPREFIX.length(), ref.bibkey().length())//
361               .append("](").append(DBLPURL)//
362               .append(ref.bibkey(), DBLPPREFIX.length(), ref.bibkey().length()).append(')').lf();
363         }
364         else {
365           out.nl().append("<!-- ").append(ref.bibkey()).append(" -->").lf();
366         }
367       }
368       out.par();
369     }
370   }
371 
documentReferences(List<Map.Entry<Reference, TreeSet<Object>>> refs, F format)372   private static <T, F extends Format<T>> F documentReferences(List<Map.Entry<Reference, TreeSet<Object>>> refs, F format) throws IOException {
373     format.init("ELKI references overview");
374     for(Map.Entry<Reference, TreeSet<Object>> pair : refs) {
375       T classdt = format.newEntry();
376       for(Object o : pair.getValue()) {
377         if(o instanceof Class<?>) {
378           format.writeClass(classdt, (Class<?>) o);
379         }
380         else if(o instanceof Package) {
381           format.writePackage(classdt, (Package) o);
382         }
383       }
384       format.writeReference(pair.getKey());
385     }
386     return format;
387   }
388 
sortedReferences()389   private static List<Map.Entry<Reference, TreeSet<Object>>> sortedReferences() {
390     Map<Reference, TreeSet<Object>> map = new HashMap<>();
391 
392     HashSet<Package> packages = new HashSet<>();
393     for(Class<?> cls : ELKIServiceRegistry.findAllImplementations(Object.class, true, false)) {
394       inspectClass(cls, map);
395       if(packages.add(cls.getPackage())) {
396         Package p = cls.getPackage();
397         addReference(p, p.getAnnotationsByType(Reference.class), map);
398       }
399     }
400     // Sort references by first class.
401     List<Map.Entry<Reference, TreeSet<Object>>> refs = new ArrayList<>(map.entrySet());
402     Collections.sort(refs, SORT_BY_FIRST_CLASS);
403     return refs;
404   }
405 
inspectClass(final Class<?> cls, Map<Reference, TreeSet<Object>> map)406   private static void inspectClass(final Class<?> cls, Map<Reference, TreeSet<Object>> map) {
407     if(cls.getSimpleName().equals("package-info")) {
408       return;
409     }
410     try {
411       addReference(cls, cls.getAnnotationsByType(Reference.class), map);
412       for(Method m : cls.getDeclaredMethods()) {
413         addReference(cls, m.getAnnotationsByType(Reference.class), map);
414       }
415       for(Field f : cls.getDeclaredFields()) {
416         addReference(cls, f.getAnnotationsByType(Reference.class), map);
417       }
418       // Inner classes
419       for(Class<?> c2 : cls.getDeclaredClasses()) {
420         inspectClass(c2, map);
421       }
422     }
423     catch(Error e) {
424       LOG.warning("Exception in finding references for class " + cls.getCanonicalName() + ": " + e, e);
425     }
426   }
427 
addReference(Object cls, Reference[] r, Map<Reference, TreeSet<Object>> map)428   private static void addReference(Object cls, Reference[] r, Map<Reference, TreeSet<Object>> map) {
429     for(Reference ref : r) {
430       TreeSet<Object> list = map.get(ref);
431       if(list == null) {
432         map.put(ref, list = new TreeSet<>(SORT_PKGS_AND_CLASSES));
433       }
434       list.add(cls);
435     }
436   }
437 
438   /**
439    * Comparator for sorting the list of classes for each reference.
440    */
441   private static final Comparator<Object> SORT_PKGS_AND_CLASSES = new Comparator<Object>() {
442     @Override
443     public int compare(Object o1, Object o2) {
444       String n1 = (o1 instanceof Class) ? ((Class<?>) o1).getName() : ((Package) o1).getName();
445       String n2 = (o2 instanceof Class) ? ((Class<?>) o2).getName() : ((Package) o2).getName();
446       return n1.compareTo(n2);
447     }
448   };
449 
450   private static Comparator<Map.Entry<Reference, TreeSet<Object>>> SORT_BY_FIRST_CLASS = new Comparator<Map.Entry<Reference, TreeSet<Object>>>() {
451     @Override
452     public int compare(Map.Entry<Reference, TreeSet<Object>> p1, Map.Entry<Reference, TreeSet<Object>> p2) {
453       final Object o1 = p1.getValue().first(), o2 = p2.getValue().first();
454       int c = SORT_PKGS_AND_CLASSES.compare(o1, o2);
455       if(c == 0) {
456         Reference r1 = p1.getKey(), r2 = p2.getKey();
457         c = compareNull(r1.title(), r2.title());
458         c = (c != 0) ? c : compareNull(r1.authors(), r2.authors());
459         c = (c != 0) ? c : compareNull(r1.booktitle(), r2.booktitle());
460       }
461       return c;
462     }
463 
464     /**
465      * Null-tolerant String comparison.
466      *
467      * @param s1 First string
468      * @param s2 Second string
469      * @return Order
470      */
471     private int compareNull(String s1, String s2) {
472       return (s1 == s2) ? 0 //
473           : (s1 == null) ? -1 //
474               : (s2 == null) ? +1 //
475                   : s1.compareTo(s2);
476     }
477   };
478 
479 }
480