001/**
002 * Copyright (C) 2006-2025 Talend Inc. - www.talend.com
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.talend.sdk.component.tools;
017
018import static java.util.Arrays.asList;
019import static java.util.Collections.emptyMap;
020import static java.util.Collections.list;
021import static java.util.Locale.ENGLISH;
022import static java.util.Optional.ofNullable;
023import static java.util.stream.Collectors.toList;
024import static org.apache.ziplock.JarLocation.jarLocation;
025
026import java.io.BufferedInputStream;
027import java.io.BufferedOutputStream;
028import java.io.File;
029import java.io.FileInputStream;
030import java.io.FileNotFoundException;
031import java.io.FileOutputStream;
032import java.io.IOException;
033import java.io.InputStream;
034import java.io.OutputStream;
035import java.nio.charset.StandardCharsets;
036import java.nio.file.Files;
037import java.nio.file.Path;
038import java.util.ArrayList;
039import java.util.Collection;
040import java.util.HashMap;
041import java.util.List;
042import java.util.Map;
043import java.util.jar.JarFile;
044import java.util.stream.Stream;
045
046import org.apache.ziplock.IO;
047import org.asciidoctor.Asciidoctor;
048import org.asciidoctor.AttributesBuilder;
049import org.asciidoctor.OptionsBuilder;
050import org.asciidoctor.SafeMode;
051import org.asciidoctor.jruby.internal.JRubyAsciidoctor;
052import org.talend.sdk.component.path.PathFactory;
053
054// indirection to not load asciidoctor if not in the classpath
055public class AsciidoctorExecutor implements AutoCloseable {
056
057    private Asciidoctor asciidoctor;
058
059    private File extractedResources;
060
061    private Runnable onClose = () -> {
062    };
063
064    public static void main(final String[] args) throws IOException {
065        final Collection<String> params = new ArrayList<>(asList(args));
066        try (final AsciidoctorExecutor executor = new AsciidoctorExecutor()) {
067            if (params.contains("--continue")) {
068                params.remove("--continue");
069                final String[] newArgs = params.toArray(new String[0]);
070                do {
071                    executor.doMain(newArgs);
072
073                    final String line = System.console().readLine();
074                    if (line == null || "exit".equalsIgnoreCase(line.trim())) {
075                        return;
076                    }
077                    executor.doMain(newArgs);
078                } while (true);
079            } else {
080                executor.doMain(args);
081            }
082        }
083    }
084
085    public void doMain(final String[] args) throws IOException {
086        final Path adoc = PathFactory.get(args[0]).toAbsolutePath();
087        final File output =
088                adoc.getParent().resolve(args.length > 1 ? args[1] : args[0].replace(".adoc", ".pdf")).toFile();
089        final List<String> lines = Files.lines(adoc).collect(toList());
090        final String version = lines
091                .stream()
092                .filter(it -> it.startsWith("v"))
093                .findFirst()
094                .map(it -> it.substring(1))
095                .orElse(args.length > 2 ? args[2] : "");
096        render(args.length > 3 ? new File(args[3]) : new File("target/pdf"), version, new Log() {
097
098            @Override
099            public void debug(final String s) {
100                // no-op
101            }
102
103            @Override
104            public void error(final String s) {
105                System.err.println(s);
106            }
107
108            @Override
109            public void info(final String s) {
110                System.out.println(s);
111            }
112        }, "pdf", adoc.toFile(), output,
113                lines
114                        .stream()
115                        .filter(it -> it.startsWith("= "))
116                        .findFirst()
117                        .orElseGet(() -> args.length > 4 ? args[4] : "Document"),
118                new HashMap<>(), args.length > 6 ? new File(args[6]) : null, args.length > 7 ? args[7] : null,
119                args.length > 5 ? args[5] : null);
120    }
121
122    public void render(final File workDir, final String version, final Log log, final String backend, final File source,
123            final File output, final String title, final Map<String, String> attributes, final File templateDir,
124            final String templateEngine) {
125        render(workDir, version, log, backend, source, output, title, attributes, templateDir, templateEngine, null);
126    }
127
128    // CHECKSTYLE:OFF
129    private void render(final File workDir, final String version, final Log log, final String backend,
130            final File source, final File output, final String title, final Map<String, String> attributes,
131            final File templateDir, final String templateEngine, final String libraries) {
132        // CHECKSTYLE:ON
133        log.info("Rendering '" + source.getName() + "' to '" + output + "'");
134        if (asciidoctor == null) {
135            asciidoctor = getAsciidoctor();
136            if (libraries != null) {
137                Stream
138                        .of(libraries.split(","))
139                        .map(String::trim)
140                        .filter(s -> !s.isEmpty())
141                        .forEach(asciidoctor::requireLibrary);
142            }
143        }
144        final OptionsBuilder optionsBuilder = OptionsBuilder
145                .options()
146                .baseDir(source.getParentFile())
147                .toFile(output)
148                .backend(backend)
149                .safe(SafeMode.UNSAFE)
150                .docType("book") // todo: config
151                .mkDirs(true);
152
153        final AttributesBuilder attrs =
154                AttributesBuilder.attributes().attributeMissing("skip").attributeUndefined("drop");
155        if (attributes == null) {
156            configureDefaultsAttributes(workDir, log, backend, attrs, emptyMap());
157        } else {
158            attributes.forEach((k, v) -> {
159                if (v == null) {
160                    attrs.attribute(k);
161                } else {
162                    attrs.attribute(k, v);
163                }
164            });
165            configureDefaultsAttributes(workDir, log, backend, attrs, attributes);
166        }
167        optionsBuilder.attributes(attrs);
168
169        ofNullable(templateDir).ifPresent(optionsBuilder::templateDir);
170        ofNullable(templateEngine).ifPresent(optionsBuilder::templateEngine);
171
172        log.debug("Options: " + optionsBuilder.asMap());
173        asciidoctor.convert(wrap(title, version, source), optionsBuilder);
174    }
175
176    private void configureDefaultsAttributes(final File workDir, final Log log, final String backend,
177            final AttributesBuilder attrs, final Map<String, String> attributes) {
178        if (extractedResources == null) {
179            extractResources(workDir, log);
180        }
181        switch (backend.toLowerCase(ENGLISH)) {
182            case "html":
183            case "html5":
184                if (!attributes.containsKey("stylesheet")) {
185                    attrs.attribute("stylesheet", "talend.css");
186                }
187                if (!attributes.containsKey("stylesdir")) {
188                    attrs.attribute("stylesdir", new File(extractedResources, "resources/html").getAbsolutePath());
189                }
190                if (!attributes.containsKey("data-uri")) {
191                    attrs.attribute("data-uri");
192                }
193                break;
194            case "pdf":
195                if (!attributes.containsKey("pdf-style")) {
196                    attrs.attribute("pdf-style", "talend.yml");
197                }
198                if (!attributes.containsKey("pdf-stylesdir")) {
199                    attrs.attribute("pdf-stylesdir", new File(extractedResources, "resources/pdf").getAbsolutePath());
200                }
201                break;
202            default:
203        }
204
205        if (!attributes.containsKey("icons")) {
206            attrs.attribute("icons", "font");
207        }
208        if (!attributes.containsKey("source-highlighter")) {
209            attrs.attribute("source-highlighter", "coderay");
210        }
211        if (!attributes.containsKey("toc")) {
212            attrs.attribute("toc", "left");
213        }
214    }
215
216    private void extractResources(final File workDir, final Log log) {
217        final boolean workdirCreated = !workDir.exists();
218        workDir.mkdirs();
219        extractedResources = new File(workDir, getClass().getSimpleName() + "_resources");
220        onClose = () -> {
221            try {
222                org.apache.ziplock.Files.remove(workdirCreated ? workDir : extractedResources);
223            } catch (final IllegalStateException e) {
224                log.error(e.getMessage());
225            }
226        };
227        final File file = jarLocation(AsciidoctorExecutor.class);
228        if (file.isDirectory()) {
229            Stream.of("html", "pdf").forEach(backend -> {
230                final File[] children = new File(file, "resources/" + backend).listFiles();
231                if (children == null) {
232                    throw new IllegalStateException("No resources folder for: " + backend);
233                }
234                Stream.of(children).forEach(child -> {
235                    try {
236                        copyResource(new File(extractedResources, "resources/" + backend + '/' + child.getName()),
237                                new FileInputStream(child));
238                    } catch (final FileNotFoundException e) {
239                        throw new IllegalStateException(e);
240                    }
241                });
242            });
243        } else {
244            try (final JarFile jar = new JarFile(file)) {
245                list(jar.entries())
246                        .stream()
247                        .filter(e -> e.getName().startsWith("resources/") && !e.isDirectory())
248                        .forEach(e -> {
249                            try {
250                                copyResource(new File(extractedResources, e.getName()), jar.getInputStream(e));
251                            } catch (final IOException e1) {
252                                throw new IllegalStateException(e1);
253                            }
254                        });
255            } catch (final IOException e) {
256                throw new IllegalStateException(e);
257            }
258        }
259    }
260
261    private void copyResource(final File out, final InputStream inputStream) {
262        out.getParentFile().mkdirs();
263        try (final InputStream is = new BufferedInputStream(inputStream);
264                final OutputStream os = new BufferedOutputStream(new FileOutputStream(out))) {
265            IO.copy(is, os);
266        } catch (final IOException e1) {
267            throw new IllegalStateException(e1);
268        }
269    }
270
271    private String wrap(final String title, final String version, final File source) {
272        try {
273            final String content = String.join("\n", Files.readAllLines(source.toPath(), StandardCharsets.UTF_8));
274            if (content.startsWith("= ")) {
275                return content;
276            }
277            return "= " + title + "\n:revnumber: " + version + "\n\n" + content;
278        } catch (final IOException e) {
279            throw new IllegalArgumentException(e);
280        }
281    }
282
283    protected Asciidoctor getAsciidoctor() {
284        return asciidoctor == null ? asciidoctor = Asciidoctor.Factory.create() : asciidoctor;
285    }
286
287    @Override
288    public void close() {
289        onClose.run();
290        if (asciidoctor != null && !Boolean.getBoolean("talend.component.tools.jruby.teardown.skip")) {
291            if (AutoCloseable.class.isInstance(asciidoctor)) {
292                try {
293                    AutoCloseable.class.cast(asciidoctor).close();
294                } catch (final Exception e) {
295                    throw new IllegalStateException(e);
296                }
297            } else if (org.asciidoctor.jruby.internal.JRubyAsciidoctor.class.isInstance(asciidoctor)) {
298                JRubyAsciidoctor.class.cast(asciidoctor).getRubyRuntime().tearDown();
299            }
300        }
301    }
302}