001/** 002 * Copyright (C) 2006-2024 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}