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.emptyList; 020import static java.util.Comparator.comparing; 021import static java.util.Optional.ofNullable; 022import static java.util.stream.Collectors.joining; 023import static java.util.stream.Collectors.toList; 024import static lombok.AccessLevel.PRIVATE; 025 026import java.io.BufferedReader; 027import java.io.BufferedWriter; 028import java.io.File; 029import java.io.FileWriter; 030import java.io.IOException; 031import java.io.InputStream; 032import java.io.InputStreamReader; 033import java.io.Writer; 034import java.lang.reflect.Array; 035import java.nio.charset.StandardCharsets; 036import java.util.ArrayList; 037import java.util.Arrays; 038import java.util.Collection; 039import java.util.Collections; 040import java.util.HashMap; 041import java.util.HashSet; 042import java.util.List; 043import java.util.Locale; 044import java.util.Map; 045import java.util.NoSuchElementException; 046import java.util.Set; 047import java.util.SortedSet; 048import java.util.StringTokenizer; 049import java.util.TreeSet; 050import java.util.stream.Collectors; 051import java.util.stream.Stream; 052 053import org.apache.xbean.finder.AnnotationFinder; 054import org.talend.sdk.component.api.meta.Documentation; 055import org.talend.sdk.component.runtime.internationalization.ParameterBundle; 056import org.talend.sdk.component.runtime.manager.ParameterMeta; 057import org.talend.sdk.component.runtime.manager.reflect.Constructors; 058import org.talend.sdk.component.runtime.manager.reflect.ParameterModelService; 059import org.talend.sdk.component.runtime.manager.reflect.parameterenricher.BaseParameterEnricher; 060import org.talend.sdk.component.runtime.manager.reflect.parameterenricher.ConditionParameterEnricher; 061import org.talend.sdk.component.runtime.manager.reflect.parameterenricher.ConfigurationTypeParameterEnricher; 062import org.talend.sdk.component.runtime.manager.reflect.parameterenricher.DocumentationParameterEnricher; 063import org.talend.sdk.component.runtime.manager.reflect.parameterenricher.UiParameterEnricher; 064import org.talend.sdk.component.runtime.manager.service.LocalConfigurationService; 065import org.talend.sdk.component.runtime.manager.util.DefaultValueInspector; 066import org.talend.sdk.component.runtime.manager.xbean.registry.EnrichedPropertyEditorRegistry; 067import org.talend.sdk.component.tools.ComponentHelper.Component; 068 069import lombok.AllArgsConstructor; 070import lombok.Data; 071import lombok.Getter; 072import lombok.RequiredArgsConstructor; 073 074public abstract class DocBaseGenerator extends BaseTask { 075 076 private final DefaultValueInspector defaultValueInspector = new DefaultValueInspector(); 077 078 @Getter 079 private final Locale locale; 080 081 private final AbsolutePathResolver resolver = new AbsolutePathResolver(); 082 083 protected final Log log; 084 085 protected final File output; 086 087 private final ParameterModelService parameterModelService = new ParameterModelService( 088 asList(new DocumentationParameterEnricher(), new ConditionParameterEnricher(), 089 new ConfigurationTypeParameterEnricher(), new UiParameterEnricher()), 090 new EnrichedPropertyEditorRegistry()) { 091 }; 092 093 DocBaseGenerator(final File[] classes, final Locale locale, final Object log, final File output) { 094 super(classes); 095 this.locale = locale; 096 this.output = output; 097 try { 098 this.log = Log.class.isInstance(log) ? Log.class.cast(log) : new ReflectiveLog(log); 099 } catch (final NoSuchMethodException e) { 100 throw new IllegalArgumentException(e); 101 } 102 } 103 104 @Override 105 public final void run() { 106 final Locale oldLocale = Locale.getDefault(); 107 final boolean shouldSwitchLocale = 108 oldLocale != Locale.ROOT && !ofNullable(oldLocale.getLanguage()).orElse("").equals("en"); 109 if (shouldSwitchLocale) { 110 Locale.setDefault(Locale.ROOT); 111 } 112 try { 113 doRun(); 114 } finally { 115 if (shouldSwitchLocale) { 116 Locale.setDefault(oldLocale); 117 } 118 } 119 } 120 121 protected abstract void doRun(); 122 123 protected Stream<ComponentDescription> components() { 124 final AnnotationFinder finder = newFinder(); 125 return findComponents(finder).map(component -> { 126 final Collection<ParameterMeta> parameterMetas = parameterModelService 127 .buildParameterMetas(Constructors.findConstructor(component), 128 ofNullable(component.getPackage()).map(Package::getName).orElse(""), 129 new BaseParameterEnricher.Context(new LocalConfigurationService(emptyList(), "tools"))); 130 final Component componentMeta = ComponentHelper 131 .componentMarkers() 132 .filter(component::isAnnotationPresent) 133 .map(component::getAnnotation) 134 .map(ComponentHelper::asComponent) 135 .findFirst() 136 .orElseThrow(NoSuchElementException::new); 137 String family = ""; 138 try { 139 family = ComponentHelper.findFamily(componentMeta, component); 140 } catch (final IllegalArgumentException iae) { 141 // skip for doc 142 } 143 return new ComponentDescription(component, family, componentMeta.name(), getDoc(component), 144 sort(parameterMetas), resolver, defaultValueInspector, locale, emptyDefaultValue()); 145 }); 146 } 147 148 protected Stream<Class<?>> findComponents(final AnnotationFinder finder) { 149 return ComponentHelper.componentMarkers().flatMap(a -> finder.findAnnotatedClasses(a).stream()); 150 } 151 152 private String getDoc(final Class<?> component) { 153 final Collection<String> docKeys = Stream 154 .of(getComponentPrefix(component), component.getSimpleName()) 155 .map(it -> it + "._documentation") 156 .collect(toList()); 157 return ofNullable(findResourceBundle(component)) 158 .map(bundle -> docKeys 159 .stream() 160 .filter(bundle::containsKey) 161 .map(bundle::getString) 162 .findFirst() 163 .map(v -> v + "\n\n") 164 .orElse(null)) 165 .orElseGet(() -> ofNullable(component.getAnnotation(Documentation.class)) 166 .map(Documentation::value) 167 .map(v -> v + "\n\n") 168 .orElse("")); 169 } 170 171 private String getComponentPrefix(final Class<?> component) { 172 try { 173 return ComponentHelper 174 .components(component) 175 .map(c -> ComponentHelper.findFamily(c, component) + "." + c.name()) 176 .orElseGet(component::getSimpleName); 177 } catch (final RuntimeException e) { 178 return component.getSimpleName(); 179 } 180 } 181 182 private Collection<ParameterMeta> sort(final Collection<ParameterMeta> parameterMetas) { 183 return parameterMetas.stream().sorted(comparing(ParameterMeta::getPath)).collect(toList()); 184 } 185 186 protected void write(final File output, final String content) { 187 ensureParentExists(output); 188 try (final Writer writer = new BufferedWriter(new FileWriter(output))) { 189 writer.write(content); 190 } catch (IOException e) { 191 throw new IllegalStateException(e); 192 } 193 } 194 195 protected void ensureParentExists(final File output) { 196 if (!output.getParentFile().isDirectory() && !output.getParentFile().mkdirs()) { 197 throw new IllegalStateException("Can't create " + output.getParentFile()); 198 } 199 } 200 201 protected String emptyDefaultValue() { 202 return "-"; 203 } 204 205 private static class AbsolutePathResolver { 206 207 // ensure it is aligned with org.talend.sdk.component.studio.model.parameter.SettingsCreator.computeTargetPath() 208 // and org.talend.sdk.component.form.internal.converter.impl.widget.path.AbsolutePathResolver 209 public String resolveProperty(final String propPath, final String paramRef) { 210 return doResolveProperty(propPath, normalizeParamRef(paramRef)); 211 } 212 213 private String normalizeParamRef(final String paramRef) { 214 return (!paramRef.contains(".") ? "../" : "") + paramRef; 215 } 216 217 private String doResolveProperty(final String propPath, final String paramRef) { 218 if (".".equals(paramRef)) { 219 return propPath; 220 } 221 if (paramRef.startsWith("..")) { 222 String current = propPath; 223 String ref = paramRef; 224 while (ref.startsWith("..")) { 225 int lastDot = current.lastIndexOf('.'); 226 if (lastDot < 0) { 227 lastDot = 0; 228 } 229 current = current.substring(0, lastDot); 230 ref = ref.substring("..".length(), ref.length()); 231 if (ref.startsWith("/")) { 232 ref = ref.substring(1); 233 } 234 if (current.isEmpty()) { 235 break; 236 } 237 } 238 return Stream.of(current, ref.replace('/', '.')).filter(it -> !it.isEmpty()).collect(joining(".")); 239 } 240 if (paramRef.startsWith(".") || paramRef.startsWith("./")) { 241 return propPath + '.' + paramRef.replaceFirst("\\./?", "").replace('/', '.'); 242 } 243 return paramRef; 244 } 245 } 246 247 @Data 248 @AllArgsConstructor(access = PRIVATE) 249 protected static class Condition { 250 251 private final String target; 252 253 private final String path; 254 255 private final String value; 256 257 private final boolean negate; 258 259 private final String strategy; 260 } 261 262 @Data 263 protected static class UIInfo { 264 265 private final String section; 266 267 private final Set<String> nestedLayout = new HashSet<>(); 268 269 public UIInfo(final String section, final Collection<String> layouts) { 270 this.section = section; 271 nestedLayout.addAll(layouts); 272 } 273 274 public void addNestedLayouts(final Collection<String> layouts) { 275 nestedLayout.addAll(layouts); 276 } 277 } 278 279 @Data 280 @RequiredArgsConstructor(access = PRIVATE) 281 protected static class Param implements Comparable<Param> { 282 283 private final String displayName; 284 285 private final String documentation; 286 287 private final String defaultValue; 288 289 private final String type; 290 291 private final String fullPath; 292 293 private final Conditions conditions; 294 295 private final String section; 296 297 private UIInfo uiInfo; 298 299 private final SortedSet<Param> nested = new TreeSet<>(); 300 301 public void addNested(final Param p) { 302 nested.add(p); 303 } 304 305 public int compareTo(final Param p) { 306 return this.getDisplayName().compareTo(p.getDisplayName()); 307 } 308 309 public boolean isComplex() { 310 return this.nested.size() > 0; 311 } 312 313 public boolean isSection() { 314 return isComplex() && section != null && !section.isEmpty(); 315 } 316 317 public String getSectionName() { 318 return "datastore".equals(section) ? "connection" : section; 319 } 320 321 } 322 323 @Data 324 @AllArgsConstructor(access = PRIVATE) 325 public static class Conditions { 326 327 private final String path; 328 329 private final String operator; 330 331 private final Collection<Condition> conditions; 332 } 333 334 @Data 335 @AllArgsConstructor(access = PRIVATE) 336 protected static class ComponentDescription { 337 338 private final Class<?> type; 339 340 private final String family; 341 342 private final String name; 343 344 private final String documentation; 345 346 private final Collection<ParameterMeta> parameters; 347 348 private final AbsolutePathResolver resolver; 349 350 private final DefaultValueInspector defaultValueInspector; 351 352 private final Locale locale; 353 354 private final String emptyDefaultValue; 355 356 private Map<String, UIInfo> getUIParamByPath(final Collection<ParameterMeta> parameters) { 357 Map<String, UIInfo> uiparams = new HashMap<>(); 358 recurseUIParam(parameters, uiparams, "", null, null); 359 360 return uiparams; 361 } 362 363 private Set<String> recurseUIParam(final Collection<ParameterMeta> params, final Map<String, UIInfo> uiparams, 364 final String currentSectionName, final ParameterMeta parent, final Collection<String> parentLayouts) { 365 Set<String> layouts = new HashSet<>(); 366 for (ParameterMeta param : params) { 367 String path = param.getPath(); 368 String sectionType = param.getMetadata().get("tcomp::configurationtype::type"); 369 if (sectionType == null) { 370 sectionType = currentSectionName; 371 } 372 // String layout = getLayout(parent, layoutParent, param); 373 Collection<String> paramLayouts = getPropertiesByLayout(parent, param.getName(), parentLayouts); 374 layouts.addAll(paramLayouts); 375 376 UIInfo uiInfo = new UIInfo(currentSectionName, paramLayouts); 377 uiparams.put(path, uiInfo); 378 if (param.getNestedParameters().size() > 0) { 379 Set<String> subLayouts = 380 recurseUIParam(param.getNestedParameters(), uiparams, sectionType, param, paramLayouts); 381 // uiInfo.addNestedLayouts(subLayouts); 382 } 383 } 384 385 return layouts; 386 } 387 388 private Collection<String> getPropertiesByLayout(final ParameterMeta parent, final String param, 389 final Collection<String> parentLayouts) { 390 final Collection<String> layouts = new TreeSet<>(); 391 392 if (parent == null) { 393 return Arrays.asList("tcomp::ui::gridlayout::Main::value", "tcomp::ui::gridlayout::Advanced::value"); 394 } 395 396 Collection<String> definedLayouts = parent 397 .getMetadata() 398 .keySet() 399 .stream() 400 .filter(k -> k.startsWith("tcomp::ui::gridlayout::")) 401 .collect(Collectors.toList()); 402 if (definedLayouts.isEmpty()) { 403 // If no layout defined, we take main if exists in parent 404 if (parentLayouts.contains("tcomp::ui::gridlayout::Main::value")) { 405 return Arrays.asList("tcomp::ui::gridlayout::Main::value"); 406 } 407 } 408 409 definedLayouts 410 .stream() 411 .filter(l -> parentLayouts.contains(l)) // A property is visible in a layout only if its parent is 412 // also visible in this layout 413 .forEach(k -> { 414 String layoutConfig = parent.getMetadata().get(k); 415 if (layoutConfig != null) { 416 boolean isInThisLayout = Collections 417 .list(new StringTokenizer(layoutConfig, "|")) 418 .stream() 419 .flatMap(p -> Collections.list(new StringTokenizer(p.toString(), ",")).stream()) 420 .collect(Collectors.toList()) 421 .contains(param); 422 423 if (isInThisLayout) { 424 layouts.add(k); 425 } 426 } 427 }); 428 429 return layouts; 430 } 431 432 Map<String, Map<String, List<Param>>> getParametersWithUInfo() { 433 final Map<String, Map<String, List<Param>>> params = new HashMap<>(); 434 final Map<String, UIInfo> uiInfos = getUIParamByPath(parameters); 435 436 for (Param p : toParams()) { 437 setParametersWithUInfoRecurs(p, uiInfos, params); 438 } 439 440 return params; 441 } 442 443 void setParametersWithUInfoRecurs(final Param p, final Map<String, UIInfo> uiInfos, 444 final Map<String, Map<String, List<Param>>> params) { 445 UIInfo info = uiInfos.get(p.fullPath); 446 p.setUiInfo(info); 447 448 Map<String, List<Param>> section = params.get(info.getSection()); 449 if (section == null) { 450 section = new HashMap<>(); 451 params.put(info.getSection(), section); 452 } 453 454 for (String l : info.getNestedLayout()) { 455 List<Param> layout = section.get(l); 456 if (layout == null) { 457 layout = new ArrayList<>(); 458 section.put(l, layout); 459 } 460 461 layout.add(p); 462 } 463 464 for (Param n : p.getNested()) { 465 setParametersWithUInfoRecurs(n, uiInfos, params); 466 } 467 } 468 469 private Collection<Param> toParams() { 470 Collection<Param> params = new ArrayList<>(); 471 for (ParameterMeta p : parameters) { 472 Param tp = toParamWithNested(p, null, null, new HashMap<>()); 473 params.add(tp); 474 } 475 476 return params; 477 } 478 479 Stream<Param> parameters() { 480 return mapParameters(parameters, null, null, new HashMap<>()); 481 } 482 483 private Stream<Param> mapParameters(final Collection<ParameterMeta> parameterMetas, 484 final DefaultValueInspector.Instance parentInstance, final ParameterBundle parentBundle, 485 final Map<String, String> types) { 486 return parameterMetas.stream().flatMap(p -> { 487 final DefaultValueInspector.Instance instance = defaultValueInspector 488 .createDemoInstance( 489 ofNullable(parentInstance).map(DefaultValueInspector.Instance::getValue).orElse(null), 490 p); 491 return Stream 492 .concat(Stream.of(toParam(p, instance, parentBundle, types)), 493 mapParameters(p.getNestedParameters(), instance, findBundle(p), types)); 494 }); 495 } 496 497 private Param toParamWithNested(final ParameterMeta p, final DefaultValueInspector.Instance parentInstance, 498 final ParameterBundle parent, final Map<String, String> types) { 499 final ParameterBundle bundle = findBundle(p); 500 final String type = findEnclosingConfigurationType(p, types); 501 502 final DefaultValueInspector.Instance instance = defaultValueInspector 503 .createDemoInstance( 504 ofNullable(parentInstance).map(DefaultValueInspector.Instance::getValue).orElse(null), p); 505 506 Param param = new Param(bundle.displayName(parent).orElse(p.getName()), 507 bundle.documentation(parent).orElseGet(() -> findDocumentation(p)), 508 ofNullable(findDefault(p, instance)).orElse(emptyDefaultValue), ofNullable(type).orElse("-"), 509 p.getPath(), createConditions(p.getPath(), p.getMetadata()), 510 p.getMetadata().get("tcomp::configurationtype::type")); 511 512 for (ParameterMeta child : p.getNestedParameters()) { 513 Param np = toParamWithNested(child, instance, bundle, types); 514 param.addNested(np); 515 } 516 517 return param; 518 } 519 520 private Param toParam(final ParameterMeta p, final DefaultValueInspector.Instance instance, 521 final ParameterBundle parent, final Map<String, String> types) { 522 final ParameterBundle bundle = findBundle(p); 523 final String type = findEnclosingConfigurationType(p, types); 524 return new Param(bundle.displayName(parent).orElse(p.getName()), 525 bundle.documentation(parent).orElseGet(() -> findDocumentation(p)), 526 ofNullable(findDefault(p, instance)).orElse(emptyDefaultValue), ofNullable(type).orElse("-"), 527 p.getPath(), createConditions(p.getPath(), p.getMetadata()), "Not computed"); 528 } 529 530 private ParameterBundle findBundle(final ParameterMeta p) { 531 return p.findBundle(Thread.currentThread().getContextClassLoader(), locale); 532 } 533 534 private String findEnclosingConfigurationType(final ParameterMeta p, final Map<String, String> types) { 535 String type = p.getMetadata().get("tcomp::configurationtype::type"); 536 if (type != null) { 537 types.put(p.getPath(), type); 538 } else { // try to find the closest parent 539 String currentPath = p.getPath(); 540 while (type == null) { 541 final int sep = currentPath.lastIndexOf('.'); 542 if (sep < 0) { 543 break; 544 } 545 currentPath = currentPath.substring(0, sep); 546 type = types.get(currentPath); 547 } 548 } 549 return type; 550 } 551 552 private String findDefault(final ParameterMeta p, final DefaultValueInspector.Instance instance) { 553 final String defVal = p.getMetadata().get("tcomp::ui::defaultvalue::value"); 554 if (defVal != null) { // todo: revise if we need to know if it is a ui default or an instance default 555 return defVal; 556 } 557 558 if (instance == null || instance.getValue() == null || instance.isCreated()) { 559 return null; 560 } 561 562 switch (p.getType()) { 563 case NUMBER: 564 case BOOLEAN: 565 case STRING: 566 case ENUM: 567 return ofNullable(instance.getValue()) 568 .map(String::valueOf) 569 .map(it -> it.isEmpty() ? "<empty>" : it) 570 .orElse(null); 571 case ARRAY: 572 return String 573 .valueOf(Collection.class.isInstance(instance.getValue()) 574 ? Collection.class.cast(instance.getValue()).size() 575 : Array.getLength(instance.getValue())); 576 case OBJECT: 577 default: 578 return null; 579 } 580 } 581 582 private Conditions createConditions(final String path, final Map<String, String> metadata) { 583 final String globalOperator = metadata.getOrDefault("tcomp::condition::ifs::operator", "AND"); 584 final Collection<Condition> conditionEntries = 585 metadata.keySet().stream().filter(it -> it.startsWith("tcomp::condition::if::target")).map(it -> { 586 final String target = metadata.get(it); 587 return new Condition(target, resolver.doResolveProperty(path, target), 588 metadata.get(it.replace("::target", "::value")), 589 Boolean.parseBoolean(metadata.get(it.replace("::target", "::negate"))), 590 metadata.get(it.replace("::target", "::evaluationStrategy"))); 591 }).collect(toList()); 592 return new Conditions(path, globalOperator, conditionEntries); 593 } 594 595 private String findDocumentation(final ParameterMeta p) { 596 final String inline = p.getMetadata().get("tcomp::documentation::value"); 597 if (inline != null) { 598 if (inline.startsWith("resource:")) { 599 final InputStream stream = Thread 600 .currentThread() 601 .getContextClassLoader() 602 .getResourceAsStream(inline.substring("resource:".length())); 603 if (stream != null) { 604 try (final BufferedReader reader = 605 new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) { 606 607 return reader.lines().collect(joining("\n")); 608 } catch (final IOException e) { 609 throw new IllegalArgumentException("Bad resource: '" + inline + "'", e); 610 } 611 } else { 612 throw new IllegalArgumentException("No resource: '" + inline + "'"); 613 } 614 615 } 616 return inline; 617 } 618 return p.getName() + " configuration"; 619 } 620 } 621}