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.Locale.ROOT;
019import static java.util.Optional.ofNullable;
020
021import java.io.ByteArrayOutputStream;
022import java.io.File;
023import java.io.IOException;
024import java.io.OutputStream;
025import java.io.StringWriter;
026import java.nio.charset.StandardCharsets;
027import java.nio.file.Files;
028import java.nio.file.StandardOpenOption;
029import java.util.Collection;
030import java.util.Collections;
031import java.util.HashMap;
032import java.util.HashSet;
033import java.util.List;
034import java.util.Locale;
035import java.util.Map;
036import java.util.concurrent.atomic.AtomicBoolean;
037import java.util.stream.Collectors;
038import java.util.stream.IntStream;
039import java.util.stream.Stream;
040import java.util.zip.ZipEntry;
041import java.util.zip.ZipOutputStream;
042
043import javax.xml.parsers.DocumentBuilder;
044import javax.xml.parsers.DocumentBuilderFactory;
045import javax.xml.parsers.ParserConfigurationException;
046import javax.xml.transform.OutputKeys;
047import javax.xml.transform.Transformer;
048import javax.xml.transform.TransformerException;
049import javax.xml.transform.TransformerFactory;
050import javax.xml.transform.dom.DOMSource;
051import javax.xml.transform.stream.StreamResult;
052
053import org.w3c.dom.Document;
054import org.w3c.dom.Element;
055
056public class DitaDocumentationGenerator extends DocBaseGenerator {
057
058    private final boolean ignoreType;
059
060    private final boolean ignoreFullPath;
061
062    public DitaDocumentationGenerator(final File[] classes, final Locale locale, final Object log, final File output,
063            final boolean ignoreType, final boolean ignoreFullPath) {
064        super(classes, locale, log, output);
065        this.ignoreType = ignoreType;
066        this.ignoreFullPath = ignoreFullPath;
067    }
068
069    @Override
070    public void doRun() {
071        final TransformerFactory transformerFactory = TransformerFactory.newInstance();
072        final DocumentBuilderFactory builderFactory = newDocFactory();
073        final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); // dont write if it fails later
074        final Collection<String> directories = new HashSet<>();
075        try (final ZipOutputStream zip = new ZipOutputStream(buffer)) {
076            components().forEach(it -> {
077                try {
078                    addDita(it, builderFactory, transformerFactory, zip, directories);
079                } catch (final ParserConfigurationException e) {
080                    throw new IllegalStateException(e);
081                }
082            });
083        } catch (final IOException e) {
084            throw new IllegalStateException(e);
085        }
086
087        ensureParentExists(output);
088        try (final OutputStream out = Files
089                .newOutputStream(output.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE,
090                        StandardOpenOption.TRUNCATE_EXISTING)) {
091            out.write(buffer.toByteArray());
092        } catch (IOException e) {
093            throw new IllegalStateException(e);
094        }
095
096        log.info("Generated " + output.getAbsolutePath());
097    }
098
099    @Override
100    protected String emptyDefaultValue() {
101        return null;
102    }
103
104    private void addDita(final ComponentDescription componentDescription, final DocumentBuilderFactory factory,
105            final TransformerFactory transformerFactory, final ZipOutputStream zip,
106            final Collection<String> directories) throws ParserConfigurationException {
107
108        final String family = componentDescription.getFamily();
109        final String name = componentDescription.getName();
110        final String componentId = family + '-' + name;
111
112        final DocumentBuilder builder = factory.newDocumentBuilder();
113        final Document xml = builder.newDocument();
114
115        final Element reference = xml.createElement("reference");
116        reference.setAttribute("id", "connector_" + componentId);
117        reference.setAttribute("id", "connector-" + family + '-' + name);
118        reference
119                .setAttribute("xml:lang",
120                        ofNullable(getLocale().getLanguage()).filter(it -> !it.isEmpty()).orElse("en-us"));
121        xml.appendChild(reference);
122
123        final Element title = xml.createElement("title");
124        title.setAttribute("id", "component_title_" + componentId);
125        title.setTextContent(name + " parameters");
126        reference.appendChild(title);
127
128        final Element shortdesc = xml.createElement("shortdesc");
129        shortdesc.setTextContent(componentDescription.getDocumentation().trim());
130        reference.appendChild(shortdesc);
131
132        final Element prolog = xml.createElement("prolog");
133        final Element metadata = xml.createElement("metadata");
134
135        final Element othermeta = xml.createElement("othermeta");
136        othermeta.setAttribute("content", family);
137        othermeta.setAttribute("name", "pageid");
138        metadata.appendChild(othermeta);
139        prolog.appendChild(metadata);
140        reference.appendChild(prolog);
141
142        final Element body = xml.createElement("refbody");
143        body.setAttribute("outputclass", "subscription");
144
145        Map<String, Map<String, List<Param>>> parametersWithUInfo2 = componentDescription.getParametersWithUInfo();
146
147        generateConfigurationSection(xml, body, family, name, reference.getAttribute("id"),
148                parametersWithUInfo2.get("datastore"), "connection");
149        generateConfigurationSection(xml, body, family, name, reference.getAttribute("id"),
150                parametersWithUInfo2.get("dataset"), "dataset");
151        generateConfigurationSection(xml, body, family, name, reference.getAttribute("id"),
152                parametersWithUInfo2.get(""), "other");
153
154        reference.appendChild(body);
155
156        final StringWriter writer = new StringWriter();
157        final StreamResult result = new StreamResult(writer);
158        try {
159            final Transformer transformer = transformerFactory.newTransformer();
160            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
161            transformer.setOutputProperty("{http://xml.apache.org/xalan}indent-amount", "2");
162            transformer.transform(new DOMSource(xml), result);
163
164            final String rootDir = output.getName().replace(".zip", "");
165            if (directories.add(rootDir)) {
166                zip.putNextEntry(new ZipEntry(rootDir + '/'));
167                zip.closeEntry();
168            }
169            final String ditaFolder = rootDir + '/' + family;
170            if (directories.add(ditaFolder)) {
171                zip.putNextEntry(new ZipEntry(ditaFolder + '/'));
172                zip.closeEntry();
173            }
174
175            final String path = ditaFolder + '/' + name + ".dita";
176            zip.putNextEntry(new ZipEntry(path));
177            final String content = writer.toString();
178            final int refIdx = content.indexOf("<reference");
179            zip
180                    .write((content.substring(0, refIdx)
181                            + "<!DOCTYPE reference PUBLIC \"-//Talend//DTD DITA Composite//EN\" \"TalendDitabase.dtd\">\n"
182                            + content.substring(refIdx)).getBytes(StandardCharsets.UTF_8));
183            zip.closeEntry();
184        } catch (final IOException | TransformerException e) {
185            throw new IllegalStateException(e);
186        }
187    }
188
189    private void generateConfigurationSection(final Document xml, final Element body, final String family,
190            final String name, final String id, final Map<String, List<Param>> parameters, final String sectionName) {
191        if (parameters == null) {
192            return;
193        }
194
195        final String sectionId = "section_" + id + "_" + sectionName;
196        final Element section = xml.createElement("section");
197        section.setAttribute("id", sectionId);
198        section.setAttribute("outputclass", "subscription");
199        body.appendChild(section);
200
201        final Element sectionTitle = xml.createElement("title");
202        sectionTitle
203                .setTextContent(sectionName.substring(0, 1).toUpperCase() + sectionName.substring(1)
204                        + " parameters for " + family + " " + name + " component.");
205        section.appendChild(sectionTitle);
206
207        generateConfigurationArray(xml, section, parameters.get("tcomp::ui::gridlayout::Main::value"), "", sectionId);
208        generateConfigurationArray(xml, section, parameters.get("tcomp::ui::gridlayout::Advanced::value"),
209                "Advanced parameters", sectionId);
210    }
211
212    private void generateConfigurationArray(final Document xml, final Element section, final List<Param> params,
213            final String caption, final String sectionId) {
214
215        if (params != null && params.size() > 0) {
216            // If only complex type that are not section, don't generate that node
217            boolean arrayIsNeeded = params
218                    .stream()
219                    .filter(p -> !p.isComplex() || p.isSection())
220                    .collect(Collectors.toList())
221                    .size() > 0;
222            if (!arrayIsNeeded) {
223                return;
224            }
225
226            final int columnNumber = 5 + 1 - Stream.of(ignoreType, ignoreFullPath).mapToInt(it -> it ? 1 : 0).sum();
227
228            final Element table = xml.createElement("table");
229            table.setAttribute("colsep", "1");
230            table.setAttribute("frame", "all");
231            table.setAttribute("rowsep", "1");
232
233            if (caption != null && !caption.trim().isEmpty()) {
234                final Element tCaption = xml.createElement("title");
235                tCaption.setTextContent(caption);
236                table.appendChild(tCaption);
237            }
238
239            final Element tgroup = xml.createElement("tgroup");
240            tgroup.setAttribute("cols", Integer.toString(columnNumber));
241            table.appendChild(tgroup);
242
243            IntStream.rangeClosed(1, columnNumber).forEach(col -> {
244                final Element colspec = xml.createElement("colspec");
245                colspec.setAttribute("colname", "c" + col);
246                colspec.setAttribute("colnum", Integer.toString(col));
247                colspec.setAttribute("colwidth", "1*");
248                tgroup.appendChild(colspec);
249            });
250
251            final Element configurationHead = xml.createElement("thead");
252            final Element headRow = xml.createElement("row");
253            appendColumn(xml, headRow, "Display Name");
254            appendColumn(xml, headRow, "Description");
255            appendColumn(xml, headRow, "Default Value");
256            appendColumn(xml, headRow, "Enabled If");
257            if (!ignoreFullPath) {
258                appendColumn(xml, headRow, "Path");
259            }
260            if (!ignoreType) {
261                appendColumn(xml, headRow, "Type");
262            }
263            configurationHead.appendChild(headRow);
264            tgroup.appendChild(configurationHead);
265
266            final Element configurationBody = xml.createElement("tbody");
267
268            params.forEach(param -> {
269                final Element row = xml.createElement("row");
270
271                String from = null;
272                String to = null;
273                if (!param.isSection() && param.isComplex()) {
274                    from = "c2";
275                    to = "c4";
276                }
277                appendColumn(xml, row, param.getDisplayName(), "uicontrol");
278
279                appendColumn(xml, row, param.getDocumentation(), null, from, to);
280                if (!param.isComplex()) {
281                    appendColumn(xml, row, param.getDefaultValue(), "userinput");
282                    final Element column = xml.createElement("entry");
283                    renderConditions(xml, column, param.getConditions());
284                    row.appendChild(column);
285                    if (!ignoreFullPath) {
286                        appendColumn(xml, row, param.getFullPath());
287                    }
288                    if (!ignoreType) {
289                        appendColumn(xml, row, param.getType());
290                    }
291                } else if (param.isSection()) {
292                    appendLink(xml, row,
293                            "#" + sectionId.substring(0, sectionId.lastIndexOf('_')) + "_" + param.getSectionName(),
294                            "See section " + param.getSectionName(), "c3", "c4");
295                }
296
297                configurationBody.appendChild(row);
298            });
299
300            tgroup.appendChild(configurationBody);
301
302            section.appendChild(table);
303        }
304    }
305
306    private void renderConditions(final Document xml, final Element container, final Conditions conditions) {
307        switch (conditions.getConditions().size()) {
308            case 0:
309                container.setTextContent("Always enabled");
310                break;
311            case 1:
312                renderCondition(xml, container, conditions.getConditions().iterator().next(), "or");
313                break;
314            default:
315                final Element listWrapper = xml.createElement("ul");
316                final Runnable conditionAppender = () -> conditions.getConditions().forEach(cond -> {
317                    final Element li = xml.createElement("li");
318                    renderCondition(xml, li, cond, conditions.getOperator());
319                    listWrapper.appendChild(li);
320                });
321                switch (conditions.getOperator().toUpperCase(ROOT)) {
322                    case "OR":
323                        container.setTextContent("One of these conditions is meet:");
324                        conditionAppender.run();
325                        break;
326                    case "AND":
327                    default:
328                        container.setTextContent("All of the following conditions are met:");
329                        conditionAppender.run();
330                }
331                container.appendChild(listWrapper);
332        }
333    }
334
335    private void renderCondition(final Document xml, final Element container,
336            final DocBaseGenerator.Condition condition, final String op) {
337        final Runnable valuesAppender = () -> {
338            final AtomicBoolean init = new AtomicBoolean();
339            Stream.of(condition.getValue().split(",")).map(v -> {
340                final Element userinput = xml.createElement("userinput");
341                userinput.setTextContent(v);
342                return userinput;
343            }).reduce(container, (wrapper, child) -> {
344                if (!init.compareAndSet(false, true)) {
345                    wrapper.appendChild(xml.createTextNode(" or "));
346                }
347                wrapper.appendChild(child);
348                return wrapper;
349            });
350        };
351        final Runnable appendPath = () -> {
352            final Element parmname = xml.createElement("parmname");
353            parmname.setTextContent(condition.getPath());
354            container.appendChild(parmname);
355        };
356        switch (ofNullable(condition.getStrategy()).orElse("default").toLowerCase(ROOT)) {
357            case "length":
358                if (condition.isNegate()) {
359                    if ("0".equals(condition.getValue())) {
360                        appendPath.run();
361                        container.appendChild(xml.createTextNode(" is not empty"));
362                    } else {
363                        container.appendChild(xml.createTextNode("the length of "));
364                        appendPath.run();
365                        container.appendChild(xml.createTextNode(" is not "));
366                        valuesAppender.run();
367                    }
368                } else if ("0".equals(condition.getValue())) {
369                    appendPath.run();
370                    container.appendChild(xml.createTextNode(" is empty"));
371                } else {
372                    container.appendChild(xml.createTextNode("the length of "));
373                    appendPath.run();
374                    container.appendChild(xml.createTextNode(" is "));
375                    valuesAppender.run();
376                }
377                break;
378            case "contains": {
379                appendPath.run();
380                if (condition.isNegate()) {
381                    container.appendChild(xml.createTextNode(" does not contain "));
382                } else {
383                    container.appendChild(xml.createTextNode(" contains "));
384                }
385                valuesAppender.run();
386                break;
387            }
388            case "contains(lowercase=true)": {
389                container.appendChild(xml.createTextNode("the lowercase value of "));
390                appendPath.run();
391                if (condition.isNegate()) {
392                    container.appendChild(xml.createTextNode(" does not contain "));
393                } else {
394                    container.appendChild(xml.createTextNode(" contains "));
395                }
396                valuesAppender.run();
397                break;
398            }
399            case "default":
400            default:
401                appendPath.run();
402                if (condition.isNegate()) {
403                    container.appendChild(xml.createTextNode(" is not equal to "));
404                } else {
405                    container.appendChild(xml.createTextNode(" is equal to "));
406                }
407                valuesAppender.run();
408        }
409    }
410
411    private void appendColumn(final Document xml, final Element row, final String value) {
412        appendColumn(xml, row, value, null);
413    }
414
415    private void appendColumn(final Document xml, final Element row, final String value, final String childNode) {
416        appendColumn(xml, row, value, childNode, null, null);
417    }
418
419    private void appendLink(final Document xml, final Element row, final String href, final String value,
420            final String spanFrom, final String spanTo) {
421        Map<String, String> attributes = new HashMap<>();
422        attributes.put("href", href);
423        appendColumn(xml, row, value, "link", attributes, spanFrom, spanTo);
424    }
425
426    private void appendColumn(final Document xml, final Element row, final String value, final String childNode,
427            final String spanFrom, final String spanTo) {
428        appendColumn(xml, row, value, childNode, Collections.emptyMap(), spanFrom, spanTo);
429    }
430
431    private void appendColumn(final Document xml, final Element row, final String value, final String childNode,
432            final Map<String, String> childAttributes, final String spanFrom, final String spanTo) {
433        final Element column = xml.createElement("entry");
434
435        if (spanFrom != null && spanTo != null) {
436            column.setAttribute("namest", spanFrom);
437            column.setAttribute("nameend", spanTo);
438        }
439
440        if (value != null) {
441            String content = value.trim();
442            content = (content == null) ? "" : content;
443
444            if (childNode != null && !childNode.trim().isEmpty()) {
445                Element control = xml.createElement(childNode);
446
447                childAttributes.forEach((k, v) -> control.setAttribute(k, v));
448
449                control.setTextContent(content);
450                column.appendChild(control);
451            } else {
452                column.setTextContent(content);
453            }
454        }
455
456        row.appendChild(column);
457    }
458
459    private DocumentBuilderFactory newDocFactory() {
460        final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
461        try {
462            factory.setFeature(javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING, true);
463            factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
464        } catch (final ParserConfigurationException e) {
465            throw new IllegalStateException(e);
466        }
467        return factory;
468    }
469}