diff --git a/com.avaloq.tools.ddk.check.core/META-INF/MANIFEST.MF b/com.avaloq.tools.ddk.check.core/META-INF/MANIFEST.MF index bf7ac3c899..9ec982cf50 100644 --- a/com.avaloq.tools.ddk.check.core/META-INF/MANIFEST.MF +++ b/com.avaloq.tools.ddk.check.core/META-INF/MANIFEST.MF @@ -27,7 +27,8 @@ Require-Bundle: org.eclipse.xtext, com.avaloq.tools.ddk.xtext, org.apache.commons.lang3, org.apache.commons.text, - org.objectweb.asm;resolution:=optional + org.objectweb.asm;resolution:=optional, + org.eclipse.equinox.app Export-Package: com.avaloq.tools.ddk.check, com.avaloq.tools.ddk.check.check, com.avaloq.tools.ddk.check.check.impl, @@ -44,6 +45,7 @@ Export-Package: com.avaloq.tools.ddk.check, com.avaloq.tools.ddk.check.scoping, com.avaloq.tools.ddk.check.serializer, com.avaloq.tools.ddk.check.services, + com.avaloq.tools.ddk.check.standalone, com.avaloq.tools.ddk.check.typing, com.avaloq.tools.ddk.check.util, com.avaloq.tools.ddk.check.validation diff --git a/com.avaloq.tools.ddk.check.core/plugin.xml b/com.avaloq.tools.ddk.check.core/plugin.xml index 7edc2a56d3..81f2b8b7cc 100644 --- a/com.avaloq.tools.ddk.check.core/plugin.xml +++ b/com.avaloq.tools.ddk.check.core/plugin.xml @@ -11,4 +11,12 @@ + + + + + + diff --git a/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/CheckDocumentationTemplates.java b/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/CheckDocumentationTemplates.java new file mode 100644 index 0000000000..de0a8eb347 --- /dev/null +++ b/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/CheckDocumentationTemplates.java @@ -0,0 +1,674 @@ +/******************************************************************************* + * Copyright (c) 2026 Avaloq Group AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Avaloq Group AG - initial API and implementation + *******************************************************************************/ +package com.avaloq.tools.ddk.check.generator; + +import java.util.List; +import java.util.Objects; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.xtend2.lib.StringConcatenation; +import org.eclipse.xtext.EcoreUtil2; +import org.eclipse.xtext.xbase.lib.IterableExtensions; + +import com.avaloq.tools.ddk.check.check.Category; +import com.avaloq.tools.ddk.check.check.Check; +import com.avaloq.tools.ddk.check.check.CheckCatalog; +import com.google.inject.Inject; + +/** + * Emits the Eclipse-Help {@code toc.xml} and context-help {@code contexts.xml} + * files for a set of {@link CheckCatalog}s as plain XML strings, plus the + * standalone-browser {@code index.html} landing page. Also exposes the shared + * {@link #STYLE} block used by both the index and the per-catalog pages. + */ +@SuppressWarnings("nls") +public class CheckDocumentationTemplates { + + private static final String TOC_LABEL = "Check Catalogs"; + private static final String TOC_ANCHOR = "../com.avaloq.tools.ddk.check.runtime.ui/toc.xml#checkdocumentation"; + private static final String DOCS_REF_PREFIX = "docs/content/"; + + /** + * Single-source-of-truth stylesheet used by the index page and every per-catalog + * HTML page. Inlined into the page {@code } so the output is self-contained + * and renders in any standard browser without external assets. + */ + public static final String STYLE = buildStyle(); + + @Inject + private CheckGeneratorNaming naming; + + @Inject + private CheckGeneratorExtensions extensions; + + /** Build the contents of {@code docs/toc.xml} aggregating every catalog in {@code catalogs}. */ + public CharSequence compileToc(final Iterable catalogs) { + final List sorted = IterableExtensions.sortBy(catalogs, CheckCatalog::getName); + StringConcatenation builder = new StringConcatenation(); + builder.append(""); + builder.newLine(); + builder.append(""); + builder.newLineIfNotEmpty(); + for (final CheckCatalog catalog : sorted) { + builder.append(" "); + builder.append(""); + builder.newLineIfNotEmpty(); + for (final Category category : catalog.getCategories()) { + builder.append(""); + builder.newLineIfNotEmpty(); + for (final Check check : category.getChecks()) { + builder.append(" "); + builder.append(""); + builder.newLineIfNotEmpty(); + builder.append(" "); + builder.append(""); + builder.newLine(); + } + builder.append(""); + builder.newLine(); + } + for (final Check check : catalog.getChecks()) { + builder.append(""); + builder.newLineIfNotEmpty(); + builder.append(""); + builder.newLine(); + } + builder.append(" "); + builder.append(""); + builder.newLine(); + } + builder.append(""); + builder.newLine(); + return builder; + } + + /** Build the contents of {@code docs/contexts.xml} aggregating every check across {@code catalogs}. */ + public CharSequence compileContexts(final Iterable catalogs) { + final List entries = IterableExtensions.sortBy( + IterableExtensions.map( + IterableExtensions.flatMap(catalogs, CheckCatalog::getAllChecks), + check -> new ContextEntry( + naming.getContextId(check), + attrEscape(check.getLabel()), + docRef(parentCatalog(check)) + "#" + naming.getContextId(check))), + entry -> entry.id); + StringConcatenation builder = new StringConcatenation(); + builder.append(""); + builder.newLine(); + builder.append(""); + builder.newLine(); + for (final ContextEntry e : entries) { + builder.append(""); + builder.newLineIfNotEmpty(); + builder.append(" "); + builder.append(""); + builder.newLineIfNotEmpty(); + builder.append(""); + builder.newLine(); + } + builder.append(""); + builder.newLine(); + return builder; + } + + /** Build the contents of {@code docs/index.html} listing every catalog with a link to its page. */ + public CharSequence compileIndex(final Iterable catalogs) { + final List sorted = IterableExtensions.sortBy(catalogs, CheckCatalog::getName); + StringConcatenation builder = new StringConcatenation(); + builder.append(""); + builder.newLine(); + builder.append(""); + builder.newLine(); + builder.append(""); + builder.newLine(); + builder.append(" "); + builder.append(""); + builder.newLine(); + builder.append(" "); + builder.append(""); + builder.newLine(); + builder.append(" "); + builder.append("Check Catalogs"); + builder.newLine(); + builder.append(" "); + builder.append(""); + builder.newLine(); + builder.append(""); + builder.newLine(); + builder.append(""); + builder.newLine(); + builder.append(" "); + builder.append("
"); + builder.newLine(); + builder.append(" "); + builder.append("

Check Catalogs

"); + builder.newLine(); + builder.append(" "); + builder.append("

"); + builder.append(sorted.size(), " "); + builder.append(" catalog"); + if (sorted.size() != 1) { + builder.append("s"); + } + builder.append(" documented.

"); + builder.newLineIfNotEmpty(); + builder.append(" "); + builder.append("
"); + builder.newLine(); + builder.append(" "); + builder.append("
"); + builder.newLine(); + builder.append(" "); + builder.append("
    "); + builder.newLine(); + for (final CheckCatalog catalog : sorted) { + builder.append(" "); + builder.append("
  • "); + builder.newLineIfNotEmpty(); + builder.append(" "); + builder.append(" "); + builder.append("

    "); + builder.append(catalog.getName(), " "); + builder.append("

    "); + builder.newLineIfNotEmpty(); + builder.append(" "); + builder.append(" "); + final String description = extensions.formatDescription(catalog.getDescription()); + builder.newLineIfNotEmpty(); + if (description != null) { + builder.append(" "); + builder.append(" "); + builder.append("

    "); + builder.append(description, " "); + builder.append("

    "); + builder.newLineIfNotEmpty(); + } + builder.append(" "); + builder.append("
  • "); + builder.newLine(); + } + builder.append(" "); + builder.append("
"); + builder.newLine(); + builder.append(" "); + builder.append("
"); + builder.newLine(); + builder.append(""); + builder.newLine(); + builder.append(""); + builder.newLine(); + return builder; + } + + private String docRef(final CheckCatalog c) { + return DOCS_REF_PREFIX + naming.docFileName(c); + } + + private CheckCatalog parentCatalog(final EObject o) { + return EcoreUtil2.getContainerOfType(o, CheckCatalog.class); + } + + /** Path used from {@code index.html} (same directory as {@code content/}). */ + private String indexRef(final CheckCatalog c) { + return "content/" + naming.docFileName(c); + } + + /** Escape characters that have special meaning inside an XML attribute value. */ + private String attrEscape(final String s) { + if (s == null) { + return null; + } + return s.replace("&", "&") + .replace("'", "'") + .replace("\"", """) + .replace("<", "<") + .replace(">", ">"); + } + + /** Builds the {@link #STYLE} stylesheet, preserving the exact CSS layout and line breaks. */ + private static String buildStyle() { + StringConcatenation builder = new StringConcatenation(); + builder.append(":root {"); + builder.newLine(); + builder.append(" "); + builder.append("--bg: #ffffff;"); + builder.newLine(); + builder.append(" "); + builder.append("--text: #1f2328;"); + builder.newLine(); + builder.append(" "); + builder.append("--text-muted: #57606a;"); + builder.newLine(); + builder.append(" "); + builder.append("--border: #d0d7de;"); + builder.newLine(); + builder.append(" "); + builder.append("--card-bg: #f6f8fa;"); + builder.newLine(); + builder.append(" "); + builder.append("--code-bg: #f6f8fa;"); + builder.newLine(); + builder.append(" "); + builder.append("--link: #0969da;"); + builder.newLine(); + builder.append(" "); + builder.append("--accent: #218bff;"); + builder.newLine(); + builder.append(" "); + builder.append("--sev-error-bg: #ffe5e5; --sev-error-text: #82071e; --sev-error-border: #ffadb0;"); + builder.newLine(); + builder.append(" "); + builder.append("--sev-warning-bg: #fff8c5; --sev-warning-text: #633c01; --sev-warning-border: #f3df5b;"); + builder.newLine(); + builder.append(" "); + builder.append("--sev-info-bg: #ddf4ff; --sev-info-text: #054a72; --sev-info-border: #80ccff;"); + builder.newLine(); + builder.append(" "); + builder.append("--sev-ignore-bg: #eaeef2; --sev-ignore-text: #57606a; --sev-ignore-border: #d0d7de;"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + builder.append("@media (prefers-color-scheme: dark) {"); + builder.newLine(); + builder.append(" "); + builder.append(":root {"); + builder.newLine(); + builder.append(" "); + builder.append("--bg: #0d1117;"); + builder.newLine(); + builder.append(" "); + builder.append("--text: #e6edf3;"); + builder.newLine(); + builder.append(" "); + builder.append("--text-muted: #8d96a0;"); + builder.newLine(); + builder.append(" "); + builder.append("--border: #30363d;"); + builder.newLine(); + builder.append(" "); + builder.append("--card-bg: #161b22;"); + builder.newLine(); + builder.append(" "); + builder.append("--code-bg: #161b22;"); + builder.newLine(); + builder.append(" "); + builder.append("--link: #58a6ff;"); + builder.newLine(); + builder.append(" "); + builder.append("--accent: #1f6feb;"); + builder.newLine(); + builder.append(" "); + builder.append("--sev-error-bg: #3d1419; --sev-error-text: #ffa198; --sev-error-border: #6e1216;"); + builder.newLine(); + builder.append(" "); + builder.append("--sev-warning-bg: #3a2c00; --sev-warning-text: #f0d97c; --sev-warning-border: #7a5a00;"); + builder.newLine(); + builder.append(" "); + builder.append("--sev-info-bg: #0c2d4a; --sev-info-text: #79c0ff; --sev-info-border: #1f6feb;"); + builder.newLine(); + builder.append(" "); + builder.append("--sev-ignore-bg: #1c2128; --sev-ignore-text: #8d96a0; --sev-ignore-border: #30363d;"); + builder.newLine(); + builder.append(" "); + builder.append("}"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + builder.append("* { box-sizing: border-box; }"); + builder.newLine(); + builder.append("body {"); + builder.newLine(); + builder.append(" "); + builder.append("font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;"); + builder.newLine(); + builder.append(" "); + builder.append("font-size: 16px;"); + builder.newLine(); + builder.append(" "); + builder.append("line-height: 1.55;"); + builder.newLine(); + builder.append(" "); + builder.append("color: var(--text);"); + builder.newLine(); + builder.append(" "); + builder.append("background: var(--bg);"); + builder.newLine(); + builder.append(" "); + builder.append("max-width: 820px;"); + builder.newLine(); + builder.append(" "); + builder.append("margin: 0 auto;"); + builder.newLine(); + builder.append(" "); + builder.append("padding: 2rem 1.25rem 4rem;"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + builder.append("h1 { font-size: 1.875rem; margin: 0 0 0.5rem; }"); + builder.newLine(); + builder.append("h2 { font-size: 1.375rem; margin: 2rem 0 1rem; padding-bottom: 0.4rem; border-bottom: 1px solid var(--border); }"); + builder.newLine(); + builder.append("h3 { font-size: 1.05rem; margin: 0; }"); + builder.newLine(); + builder.append("p { margin: 0.5rem 0; }"); + builder.newLine(); + builder.append("a { color: var(--link); text-decoration: none; }"); + builder.newLine(); + builder.append("a:hover { text-decoration: underline; }"); + builder.newLine(); + builder.append("header.catalog-header { margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--border); }"); + builder.newLine(); + builder.append("header.catalog-header p { color: var(--text-muted); }"); + builder.newLine(); + builder.append("a.back-link { display: inline-block; margin-bottom: 0.75rem; font-size: 0.875rem; color: var(--text-muted); }"); + builder.newLine(); + builder.append("a.back-link:hover { color: var(--link); }"); + builder.newLine(); + builder.append("nav.jump {"); + builder.newLine(); + builder.append(" "); + builder.append("margin-top: 1rem;"); + builder.newLine(); + builder.append(" "); + builder.append("padding: 0.75rem 1rem;"); + builder.newLine(); + builder.append(" "); + builder.append("background: var(--card-bg);"); + builder.newLine(); + builder.append(" "); + builder.append("border: 1px solid var(--border);"); + builder.newLine(); + builder.append(" "); + builder.append("border-radius: 6px;"); + builder.newLine(); + builder.append(" "); + builder.append("font-size: 0.9rem;"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + builder.append("nav.jump strong { display: block; margin-bottom: 0.4rem; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); }"); + builder.newLine(); + builder.append("nav.jump ul { margin: 0; padding-left: 1.25rem; }"); + builder.newLine(); + builder.append("nav.jump li { margin: 0.15rem 0; }"); + builder.newLine(); + builder.append("section.category { margin: 2rem 0; }"); + builder.newLine(); + builder.append("section.category > p { color: var(--text-muted); margin-bottom: 0.75rem; }"); + builder.newLine(); + builder.append("article.check {"); + builder.newLine(); + builder.append(" "); + builder.append("margin: 0.75rem 0;"); + builder.newLine(); + builder.append(" "); + builder.append("padding: 0.85rem 1.15rem;"); + builder.newLine(); + builder.append(" "); + builder.append("background: var(--card-bg);"); + builder.newLine(); + builder.append(" "); + builder.append("border: 1px solid var(--border);"); + builder.newLine(); + builder.append(" "); + builder.append("border-radius: 6px;"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + builder.append("article.check > header {"); + builder.newLine(); + builder.append(" "); + builder.append("display: flex;"); + builder.newLine(); + builder.append(" "); + builder.append("align-items: baseline;"); + builder.newLine(); + builder.append(" "); + builder.append("gap: 0.6rem;"); + builder.newLine(); + builder.append(" "); + builder.append("flex-wrap: wrap;"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + builder.append("article.check a.anchor {"); + builder.newLine(); + builder.append(" "); + builder.append("visibility: hidden;"); + builder.newLine(); + builder.append(" "); + builder.append("color: var(--text-muted);"); + builder.newLine(); + builder.append(" "); + builder.append("font-weight: 400;"); + builder.newLine(); + builder.append(" "); + builder.append("font-size: 0.85em;"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + builder.append("article.check > header:hover a.anchor { visibility: visible; }"); + builder.newLine(); + builder.append("article.check:target {"); + builder.newLine(); + builder.append(" "); + builder.append("border-color: var(--accent);"); + builder.newLine(); + builder.append(" "); + builder.append("box-shadow: 0 0 0 3px rgba(33, 139, 255, 0.18);"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + builder.append(".severity {"); + builder.newLine(); + builder.append(" "); + builder.append("display: inline-block;"); + builder.newLine(); + builder.append(" "); + builder.append("padding: 0.1rem 0.6rem;"); + builder.newLine(); + builder.append(" "); + builder.append("font-size: 0.72rem;"); + builder.newLine(); + builder.append(" "); + builder.append("font-weight: 600;"); + builder.newLine(); + builder.append(" "); + builder.append("text-transform: uppercase;"); + builder.newLine(); + builder.append(" "); + builder.append("letter-spacing: 0.05em;"); + builder.newLine(); + builder.append(" "); + builder.append("border-radius: 999px;"); + builder.newLine(); + builder.append(" "); + builder.append("border: 1px solid transparent;"); + builder.newLine(); + builder.append(" "); + builder.append("white-space: nowrap;"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + builder.append(".severity.sev-error { background: var(--sev-error-bg); color: var(--sev-error-text); border-color: var(--sev-error-border); }"); + builder.newLine(); + builder.append(".severity.sev-warning { background: var(--sev-warning-bg); color: var(--sev-warning-text); border-color: var(--sev-warning-border); }"); + builder.newLine(); + builder.append(".severity.sev-info { background: var(--sev-info-bg); color: var(--sev-info-text); border-color: var(--sev-info-border); }"); + builder.newLine(); + builder.append(".severity.sev-ignore { background: var(--sev-ignore-bg); color: var(--sev-ignore-text); border-color: var(--sev-ignore-border); }"); + builder.newLine(); + builder.append("pre.message {"); + builder.newLine(); + builder.append(" "); + builder.append("margin: 0.6rem 0 0;"); + builder.newLine(); + builder.append(" "); + builder.append("padding: 0.55rem 0.8rem;"); + builder.newLine(); + builder.append(" "); + builder.append("background: var(--code-bg);"); + builder.newLine(); + builder.append(" "); + builder.append("border: 1px solid var(--border);"); + builder.newLine(); + builder.append(" "); + builder.append("border-radius: 6px;"); + builder.newLine(); + builder.append(" "); + builder.append("font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace;"); + builder.newLine(); + builder.append(" "); + builder.append("font-size: 0.85rem;"); + builder.newLine(); + builder.append(" "); + builder.append("white-space: pre-wrap;"); + builder.newLine(); + builder.append(" "); + builder.append("word-break: break-word;"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + builder.append("pre.message::before {"); + builder.newLine(); + builder.append(" "); + builder.append("content: \"Message\";"); + builder.newLine(); + builder.append(" "); + builder.append("display: block;"); + builder.newLine(); + builder.append(" "); + builder.append("font-family: inherit;"); + builder.newLine(); + builder.append(" "); + builder.append("font-size: 0.68rem;"); + builder.newLine(); + builder.append(" "); + builder.append("font-weight: 600;"); + builder.newLine(); + builder.append(" "); + builder.append("text-transform: uppercase;"); + builder.newLine(); + builder.append(" "); + builder.append("letter-spacing: 0.05em;"); + builder.newLine(); + builder.append(" "); + builder.append("color: var(--text-muted);"); + builder.newLine(); + builder.append(" "); + builder.append("margin-bottom: 0.2rem;"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + builder.append("ul.catalog-list { list-style: none; padding: 0; margin: 1.5rem 0 0; }"); + builder.newLine(); + builder.append("ul.catalog-list li {"); + builder.newLine(); + builder.append(" "); + builder.append("margin: 0.75rem 0;"); + builder.newLine(); + builder.append(" "); + builder.append("padding: 1rem 1.25rem;"); + builder.newLine(); + builder.append(" "); + builder.append("background: var(--card-bg);"); + builder.newLine(); + builder.append(" "); + builder.append("border: 1px solid var(--border);"); + builder.newLine(); + builder.append(" "); + builder.append("border-radius: 6px;"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + builder.append("ul.catalog-list li h2 { margin: 0; border-bottom: none; padding-bottom: 0; font-size: 1.2rem; }"); + builder.newLine(); + builder.append("ul.catalog-list li p { color: var(--text-muted); margin: 0.25rem 0 0; }"); + builder.newLine(); + return builder.toString(); + } + + /** Immutable record of a single context-help entry, sortable by id. */ + private static final class ContextEntry { + private final String id; + private final String label; + private final String href; + + ContextEntry(final String id, final String label, final String href) { + this.id = id; + this.label = label; + this.href = href; + } + + @Override + public int hashCode() { + return Objects.hash(id, label, href); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ContextEntry other = (ContextEntry) obj; + return Objects.equals(id, other.id) && Objects.equals(label, other.label) && Objects.equals(href, other.href); + } + + @Override + public String toString() { + return "ContextEntry [id=" + id + ", label=" + label + ", href=" + href + "]"; + } + } +} diff --git a/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/CheckGenerator.java b/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/CheckGenerator.java new file mode 100644 index 0000000000..fc7084b8f9 --- /dev/null +++ b/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/CheckGenerator.java @@ -0,0 +1,474 @@ +/******************************************************************************* + * Copyright (c) 2016 Avaloq Group AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Avaloq Group AG - initial API and implementation + *******************************************************************************/ +package com.avaloq.tools.ddk.check.generator; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.xtend2.lib.StringConcatenation; +import org.eclipse.xtext.common.types.JvmField; +import org.eclipse.xtext.generator.AbstractFileSystemAccess; +import org.eclipse.xtext.generator.IFileSystemAccess; +import org.eclipse.xtext.generator.IFileSystemAccess2; +import org.eclipse.xtext.generator.OutputConfiguration; +import org.eclipse.xtext.xbase.compiler.GeneratorConfig; +import org.eclipse.xtext.xbase.compiler.JvmModelGenerator; +import org.eclipse.xtext.xbase.compiler.output.ITreeAppendable; +import org.eclipse.xtext.xbase.lib.IterableExtensions; +import org.eclipse.xtext.xbase.lib.IteratorExtensions; +import org.eclipse.xtext.xbase.lib.StringExtensions; + +import com.avaloq.tools.ddk.check.check.Category; +import com.avaloq.tools.ddk.check.check.Check; +import com.avaloq.tools.ddk.check.check.CheckCatalog; +import com.avaloq.tools.ddk.check.check.FormalParameter; +import com.avaloq.tools.ddk.check.check.XIssueExpression; +import com.avaloq.tools.ddk.check.compiler.CheckGeneratorConfig; +import com.avaloq.tools.ddk.check.compiler.ICheckGeneratorConfigProvider; +import com.avaloq.tools.ddk.check.util.CheckUtil; +import com.google.common.collect.Iterables; +import com.google.inject.Inject; + + +@SuppressWarnings("nls") +public class CheckGenerator extends JvmModelGenerator { + + @Inject + private CheckGeneratorExtensions generatorExtensions; + + @Inject + private CheckGeneratorNaming naming; + + @Inject + private CheckCompiler compiler; + + @Inject + private ICheckGeneratorConfigProvider generatorConfigProvider; + + @Override + public void doGenerate(final Resource resource, final IFileSystemAccess fsa) { + final LfNormalizingFileSystemAccess lfFsa = new LfNormalizingFileSystemAccess((IFileSystemAccess2) fsa); + super.doGenerate(resource, lfFsa); // Generate validator, catalog, and preference initializer from inferred Jvm models. + final CheckGeneratorConfig config = generatorConfigProvider.get(resource == null ? null : resource.getURI()); + for (final CheckCatalog catalog : Iterables.filter(IteratorExtensions.toIterable(resource.getAllContents()), CheckCatalog.class)) { + lfFsa.generateFile(naming.issueCodesFilePath(catalog), compileIssueCodes(catalog)); + lfFsa.generateFile(naming.standaloneSetupPath(catalog), compileStandaloneSetup(catalog)); + + // change output path for service registry + lfFsa.generateFile( + CheckUtil.serviceRegistryClassName(), + CheckGeneratorConstants.CHECK_REGISTRY_OUTPUT, + generateServiceRegistry(catalog, CheckUtil.serviceRegistryClassName(), fsa)); + // generate documentation for SCA-checks only + if (config != null && (config.doGenerateDocumentationForAllChecks() || !config.isGenerateLanguageInternalChecks())) { + // change output path for html files to docs/ + lfFsa.generateFile(naming.docFileName(catalog), CheckGeneratorConstants.CHECK_DOC_OUTPUT, compileDoc(catalog)); + } + } + } + + /** Documentation compiler, generates a self-contained HTML page per catalog. */ + public CharSequence compileDoc(final CheckCatalog catalog) { + StringConcatenation builder = new StringConcatenation(); + builder.append(""); + builder.newLine(); + builder.append(""); + builder.newLine(); + builder.append(""); + builder.newLine(); + builder.append(" "); + builder.append(""); + builder.newLine(); + builder.append(" "); + builder.append(""); + builder.newLine(); + builder.append(" "); + builder.append(""); + builder.append(catalog.getName(), " "); + builder.append(""); + builder.newLineIfNotEmpty(); + builder.append(" "); + builder.append(""); + builder.newLine(); + builder.append(""); + builder.newLine(); + builder.append(""); + builder.newLine(); + builder.append(" "); + builder.append("
"); + builder.newLine(); + builder.append(" "); + builder.append("← All catalogs"); + builder.newLineIfNotEmpty(); + builder.append(" "); + builder.append("

"); + builder.append(catalog.getName(), " "); + builder.append("

"); + builder.newLineIfNotEmpty(); + builder.append(" "); + final String formattedDescription = generatorExtensions.formatDescription(catalog.getDescription()); + builder.newLineIfNotEmpty(); + if (formattedDescription != null) { + builder.append(" "); + builder.append("

"); + builder.append(formattedDescription, " "); + builder.append("

"); + builder.newLineIfNotEmpty(); + } + if (!catalog.getChecks().isEmpty() || !catalog.getCategories().isEmpty()) { + builder.append(" "); + builder.append(""); + builder.newLine(); + } + builder.append(" "); + builder.append("
"); + builder.newLine(); + builder.append(" "); + builder.append("
"); + builder.newLine(); + builder.append(" "); + builder.append(bodyDoc(catalog), " "); + builder.newLineIfNotEmpty(); + builder.append(" "); + builder.append("
"); + builder.newLine(); + builder.append(""); + builder.newLine(); + builder.append(""); + builder.newLine(); + return builder; + } + + public CharSequence bodyDoc(final CheckCatalog catalog) { + StringConcatenation builder = new StringConcatenation(); + for (final Check check : catalog.getChecks()) { + builder.append(checkArticle(check)); + builder.newLineIfNotEmpty(); + } + for (final Category category : catalog.getCategories()) { + builder.append("
"); + builder.newLine(); + builder.append(" "); + builder.append("

"); + builder.append(category.getLabel(), " "); + builder.append("

"); + builder.newLineIfNotEmpty(); + builder.append(" "); + final String formattedCategoryDescription = generatorExtensions.formatDescription(category.getDescription()); + builder.newLineIfNotEmpty(); + if (formattedCategoryDescription != null) { + builder.append(" "); + builder.append("

"); + builder.append(formattedCategoryDescription, " "); + builder.append("

"); + builder.newLineIfNotEmpty(); + } + for (final Check check : category.getChecks()) { + builder.append(" "); + builder.append(checkArticle(check), " "); + builder.newLineIfNotEmpty(); + } + builder.append("
"); + builder.newLine(); + } + return builder; + } + + private CharSequence checkArticle(final Check check) { + StringConcatenation builder = new StringConcatenation(); + builder.append("
"); + builder.newLineIfNotEmpty(); + builder.append(" "); + builder.append("
"); + builder.newLine(); + builder.append(" "); + builder.append("

"); + builder.append(check.getLabel(), " "); + builder.append(" #

"); + builder.newLineIfNotEmpty(); + builder.append(" "); + builder.append(""); + builder.append(check.getDefaultSeverity().name().toLowerCase(), " "); + builder.append(""); + builder.newLineIfNotEmpty(); + builder.append(" "); + builder.append("
"); + builder.newLine(); + builder.append(" "); + final String formattedCheckDescription = generatorExtensions.formatDescription(check.getDescription()); + builder.newLineIfNotEmpty(); + if (formattedCheckDescription != null) { + builder.append(" "); + builder.append(formattedCheckDescription, " "); + builder.newLineIfNotEmpty(); + } + builder.append(" "); + builder.append("
");
+    builder.append(generatorExtensions.replacePlaceholder(check.getMessage()), "  ");
+    builder.append("
"); + builder.newLineIfNotEmpty(); + builder.append("
"); + builder.newLine(); + return builder; + } + + /** + * Creates an IssueCodes file for a Check Catalog. Every Check Catalog will have its own file + * of issue codes. + */ + public CharSequence compileIssueCodes(final CheckCatalog catalog) { + final Iterable allIssues = generatorExtensions.checkAndImplementationIssues(catalog); + final Map allIssueNames = IterableExtensions.toMap(allIssues, CheckGeneratorExtensions::issueCode, CheckGeneratorExtensions::issueName); + StringConcatenation builder = new StringConcatenation(); + if (!StringExtensions.isNullOrEmpty(catalog.getPackageName())) { + builder.append("package "); + builder.append(catalog.getPackageName()); + builder.append(";"); + builder.newLineIfNotEmpty(); + } + builder.newLine(); + builder.append("/**"); + builder.newLine(); + builder.append(" "); + builder.append("* Issue codes which may be used to address validation issues (for instance in quickfixes)."); + builder.newLine(); + builder.append(" "); + builder.append("*/"); + builder.newLine(); + builder.append("@SuppressWarnings(\"all\")"); + builder.newLine(); + builder.append("public final class "); + builder.append(CheckGeneratorNaming.issueCodesClassName(catalog)); + builder.append(" {"); + builder.newLineIfNotEmpty(); + builder.newLine(); + for (final String issueCode : IterableExtensions.sort(allIssueNames.keySet())) { + builder.append(" "); + builder.append("public static final String "); + builder.append(issueCode, " "); + builder.append(" = \""); + builder.append(CheckGeneratorExtensions.issueCodeValue(catalog, allIssueNames.get(issueCode)), " "); + builder.append("\";"); + builder.newLineIfNotEmpty(); + } + builder.newLine(); + builder.append(" "); + builder.append("private "); + builder.append(CheckGeneratorNaming.issueCodesClassName(catalog), " "); + builder.append("() {"); + builder.newLineIfNotEmpty(); + builder.append(" "); + builder.append("// Prevent instantiation."); + builder.newLine(); + builder.append(" "); + builder.append("}"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + return builder; + } + + /** Generates the Java standalone setup class which will be called by the ServiceRegistry. */ + public CharSequence compileStandaloneSetup(final CheckCatalog catalog) { + StringConcatenation builder = new StringConcatenation(); + if (!StringExtensions.isNullOrEmpty(catalog.getPackageName())) { + builder.append("package "); + builder.append(catalog.getPackageName()); + builder.append(";"); + builder.newLineIfNotEmpty(); + } + builder.newLine(); + builder.append("import org.apache.logging.log4j.Logger;"); + builder.newLine(); + builder.append("import org.apache.logging.log4j.LogManager;"); + builder.newLine(); + builder.newLine(); + builder.append("import com.avaloq.tools.ddk.check.runtime.configuration.ModelLocation;"); + builder.newLine(); + builder.append("import com.avaloq.tools.ddk.check.runtime.registry.ICheckCatalogRegistry;"); + builder.newLine(); + builder.append("import com.avaloq.tools.ddk.check.runtime.registry.ICheckValidatorRegistry;"); + builder.newLine(); + builder.append("import com.avaloq.tools.ddk.check.runtime.registry.ICheckValidatorStandaloneSetup;"); + builder.newLine(); + builder.newLine(); + builder.append("/**"); + builder.newLine(); + builder.append(" "); + builder.append("* Standalone setup for "); + builder.append(catalog.getName(), " "); + builder.append(" as required by the standalone builder."); + builder.newLineIfNotEmpty(); + builder.append(" "); + builder.append("*/"); + builder.newLine(); + builder.append("@SuppressWarnings(\"nls\")"); + builder.newLine(); + builder.append("public class "); + builder.append(naming.standaloneSetupClassName(catalog)); + builder.append(" implements ICheckValidatorStandaloneSetup {"); + builder.newLineIfNotEmpty(); + builder.newLine(); + builder.append(" "); + builder.append("private static final Logger LOG = LogManager.getLogger("); + builder.append(naming.standaloneSetupClassName(catalog), " "); + builder.append(".class);"); + builder.newLineIfNotEmpty(); + if (catalog.getGrammar() != null) { + builder.append(" "); + builder.append("private static final String GRAMMAR_NAME = \""); + builder.append(catalog.getGrammar().getName(), " "); + builder.append("\";"); + builder.newLineIfNotEmpty(); + } + builder.append(" "); + builder.append("private static final String CATALOG_FILE_PATH = \""); + builder.append(naming.checkFilePath(catalog), " "); + builder.append("\";"); + builder.newLineIfNotEmpty(); + builder.newLine(); + builder.append(" "); + builder.append("@Override"); + builder.newLine(); + builder.append(" "); + builder.append("public void doSetup() {"); + builder.newLine(); + builder.append(" "); + builder.append("ICheckValidatorRegistry.INSTANCE.registerValidator("); + if (catalog.getGrammar() != null) { + builder.append("GRAMMAR_NAME,"); + } + builder.append(" new "); + builder.append(naming.validatorClassName(catalog), " "); + builder.append("());"); + builder.newLineIfNotEmpty(); + builder.append(" "); + builder.append("ICheckCatalogRegistry.INSTANCE.registerCatalog("); + if (catalog.getGrammar() != null) { + builder.append("GRAMMAR_NAME,"); + } + builder.append(" new ModelLocation("); + builder.newLineIfNotEmpty(); + builder.append(" "); + builder.append(naming.standaloneSetupClassName(catalog), " "); + builder.append(".class.getClassLoader().getResource(CATALOG_FILE_PATH), CATALOG_FILE_PATH));"); + builder.newLineIfNotEmpty(); + builder.append(" "); + builder.append("LOG.info(\"Standalone setup done for "); + builder.append(naming.checkFilePath(catalog), " "); + builder.append("\");"); + builder.newLineIfNotEmpty(); + builder.append(" "); + builder.append("}"); + builder.newLine(); + builder.newLine(); + builder.append(" "); + builder.append("@Override"); + builder.newLine(); + builder.append(" "); + builder.append("public String toString() {"); + builder.newLine(); + builder.append(" "); + builder.append("return \"CheckValidatorSetup("); + builder.append(catalog.eResource().getURI().path(), " "); + builder.append(")\";"); + builder.newLineIfNotEmpty(); + builder.append(" "); + builder.append("}"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + return builder; + } + + /** + * Writes contents of the service registry file containing fully qualified class names of all validators. + * See also http://docs.oracle.com/javase/1.4.2/docs/api/javax/imageio/spi/ServiceRegistry.html + */ + public CharSequence generateServiceRegistry(final CheckCatalog catalog, final String serviceRegistryFileName, final IFileSystemAccess fsa) { + final OutputConfiguration config = ((AbstractFileSystemAccess) fsa).getOutputConfigurations().get(CheckGeneratorConstants.CHECK_REGISTRY_OUTPUT); + final String path = config.getOutputDirectory() + "/" + serviceRegistryFileName; + final Set contents = generatorExtensions.getContents(catalog, path); + contents.add(naming.qualifiedStandaloneSetupClassName(catalog)); + StringConcatenation builder = new StringConcatenation(); + for (final String c : contents) { + builder.append(c); + builder.newLineIfNotEmpty(); + } + return builder; + } + + @Override + public ITreeAppendable _generateMember(final JvmField field, final ITreeAppendable appendable, final GeneratorConfig config) { + // Suppress generation of the "artificial" fields for FormalParameters in check impls, but not elsewhere. + if (field.isFinal() && !field.isStatic()) { // A bit hacky to use this as the distinction... + final FormalParameter parameter = compiler.getFormalParameter(field); + if (parameter != null) { + return appendable; + } + } + return super._generateMember(field, appendable, config); + } +} diff --git a/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/CheckGenerator.xtend b/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/CheckGenerator.xtend deleted file mode 100644 index 083ddc9b25..0000000000 --- a/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/CheckGenerator.xtend +++ /dev/null @@ -1,217 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2016 Avaloq Group AG and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - * - * Contributors: - * Avaloq Group AG - initial API and implementation - *******************************************************************************/ -package com.avaloq.tools.ddk.check.generator - -import com.avaloq.tools.ddk.check.check.CheckCatalog -import com.avaloq.tools.ddk.check.util.CheckUtil -import com.google.inject.Inject -import org.eclipse.emf.ecore.resource.Resource -import org.eclipse.xtext.generator.AbstractFileSystemAccess -import org.eclipse.xtext.generator.IFileSystemAccess -import org.eclipse.xtext.generator.IFileSystemAccess2 -import org.eclipse.xtext.xbase.compiler.JvmModelGenerator - -import static org.eclipse.xtext.xbase.lib.IteratorExtensions.* -import com.avaloq.tools.ddk.check.check.FormalParameter -import org.eclipse.xtext.xbase.compiler.output.ITreeAppendable -import org.eclipse.xtext.common.types.JvmField -import org.eclipse.xtext.xbase.compiler.GeneratorConfig - -import static extension com.avaloq.tools.ddk.check.generator.CheckGeneratorExtensions.* -import static extension com.avaloq.tools.ddk.check.generator.CheckGeneratorNaming.* -import com.avaloq.tools.ddk.check.compiler.ICheckGeneratorConfigProvider - -class CheckGenerator extends JvmModelGenerator { - - @Inject extension CheckGeneratorExtensions generatorExtensions - @Inject extension CheckGeneratorNaming - @Inject CheckCompiler compiler - @Inject ICheckGeneratorConfigProvider generatorConfigProvider; - - override void doGenerate(Resource resource, IFileSystemAccess fsa) { - val lfFsa = new LfNormalizingFileSystemAccess(fsa as IFileSystemAccess2) - super.doGenerate(resource, lfFsa); // Generate validator, catalog, and preference initializer from inferred Jvm models. - val config = generatorConfigProvider.get(resource?.URI); - for (catalog : toIterable(resource.allContents).filter(typeof(CheckCatalog))) { - - lfFsa.generateFile(catalog.issueCodesFilePath, catalog.compileIssueCodes) - lfFsa.generateFile(catalog.standaloneSetupPath, catalog.compileStandaloneSetup) - - // change output path for service registry - lfFsa.generateFile( - CheckUtil::serviceRegistryClassName, - CheckGeneratorConstants::CHECK_REGISTRY_OUTPUT, - catalog.generateServiceRegistry(CheckUtil::serviceRegistryClassName, fsa) - ) - // generate documentation for SCA-checks only - if(config !== null && (config.doGenerateDocumentationForAllChecks || !config.generateLanguageInternalChecks)){ - // change output path for html files to docs/ - lfFsa.generateFile(catalog.docFileName, CheckGeneratorConstants::CHECK_DOC_OUTPUT, catalog.compileDoc) - } - } - } - - /* Documentation compiler, generates HTML output. */ - def compileDoc (CheckCatalog catalog)''' - «val body = bodyDoc(catalog)» - - - - - - «catalog.name» - - - -

Check Catalog «catalog.name»

- «val formattedDescription = catalog.description.formatDescription» - «IF formattedDescription !== null» -

«formattedDescription»

- «ENDIF» - «body» - - - - ''' - - def bodyDoc(CheckCatalog catalog)''' - «FOR check:catalog.checks» -

«check.label» («check.defaultSeverity.name().toLowerCase»)

- «val formattedCheckDescription = check.description.formatDescription» - «IF formattedCheckDescription !== null» - «formattedCheckDescription» - «ENDIF» -

Message: «check.message.replacePlaceholder»


- «ENDFOR» - «FOR category:catalog.categories» -
-

«category.label»

- «val formattedCateogryDescription = category.description.formatDescription» - «IF formattedCateogryDescription !== null» - «formattedCateogryDescription» - «ENDIF» - «FOR check:category.checks» -
-

«check.label» («check.defaultSeverity.name().toLowerCase»)

- «val formattedCheckDescription = check.description.formatDescription» - «IF formattedCheckDescription !== null» - «formattedCheckDescription» - «ENDIF» -

Message: «check.message.replacePlaceholder»

-
- «ENDFOR» -
- «ENDFOR» - ''' - - /* - * Creates an IssueCodes file for a Check Catalog. Every Check Catalog will have its own file - * of issue codes. - */ - def compileIssueCodes(CheckCatalog catalog) { - val allIssues = catalog.checkAndImplementationIssues // all Issue instances - val allIssueNames = allIssues.toMap([issue|issue.issueCode()], [issue|issue.issueName()]) // *all* issue names, unordered - - ''' - «IF !(catalog.packageName.isNullOrEmpty)» - package «catalog.packageName»; - «ENDIF» - - /** - * Issue codes which may be used to address validation issues (for instance in quickfixes). - */ - @SuppressWarnings("all") - public final class «catalog.issueCodesClassName» { - - «FOR issueCode:allIssueNames.keySet.sort» - public static final String «issueCode» = "«issueCodeValue(catalog, allIssueNames.get(issueCode))»"; - «ENDFOR» - - private «catalog.issueCodesClassName»() { - // Prevent instantiation. - } - } - ''' - } - - /* - * Generates the Java standalone setup class which will be called by the ServiceRegistry. - */ - def compileStandaloneSetup(CheckCatalog catalog) { - ''' - «IF !(catalog.packageName.isNullOrEmpty)» - package «catalog.packageName»; - «ENDIF» - - import org.apache.logging.log4j.Logger; - import org.apache.logging.log4j.LogManager; - - import com.avaloq.tools.ddk.check.runtime.configuration.ModelLocation; - import com.avaloq.tools.ddk.check.runtime.registry.ICheckCatalogRegistry; - import com.avaloq.tools.ddk.check.runtime.registry.ICheckValidatorRegistry; - import com.avaloq.tools.ddk.check.runtime.registry.ICheckValidatorStandaloneSetup; - - /** - * Standalone setup for «catalog.name» as required by the standalone builder. - */ - @SuppressWarnings("nls") - public class «catalog.standaloneSetupClassName» implements ICheckValidatorStandaloneSetup { - - private static final Logger LOG = LogManager.getLogger(«catalog.standaloneSetupClassName».class); - «IF catalog.grammar !== null» - private static final String GRAMMAR_NAME = "«catalog.grammar.name»"; - «ENDIF» - private static final String CATALOG_FILE_PATH = "«catalog.checkFilePath»"; - - @Override - public void doSetup() { - ICheckValidatorRegistry.INSTANCE.registerValidator(«IF catalog.grammar !== null»GRAMMAR_NAME,«ENDIF» new «catalog.validatorClassName»()); - ICheckCatalogRegistry.INSTANCE.registerCatalog(«IF catalog.grammar !== null»GRAMMAR_NAME,«ENDIF» new ModelLocation( - «catalog.standaloneSetupClassName».class.getClassLoader().getResource(CATALOG_FILE_PATH), CATALOG_FILE_PATH)); - LOG.info("Standalone setup done for «catalog.checkFilePath»"); - } - - @Override - public String toString() { - return "CheckValidatorSetup(«catalog.eResource.URI.path»)"; - } - } - ''' - } - - /* - * Writes contents of the service registry file containing fully qualified class names of all validators. - * See also http://docs.oracle.com/javase/1.4.2/docs/api/javax/imageio/spi/ServiceRegistry.html - */ - def generateServiceRegistry(CheckCatalog catalog, String serviceRegistryFileName, IFileSystemAccess fsa) { - val config = (fsa as AbstractFileSystemAccess).outputConfigurations.get(CheckGeneratorConstants::CHECK_REGISTRY_OUTPUT) - val path = config.outputDirectory + "/" + serviceRegistryFileName - val contents = catalog.getContents(path) - contents.add(catalog.qualifiedStandaloneSetupClassName) - ''' - «FOR c:contents» - «c» - «ENDFOR» - ''' - } - - override ITreeAppendable _generateMember(JvmField field, ITreeAppendable appendable, GeneratorConfig config) { - // Suppress generation of the "artificial" fields for FormalParameters in check impls, but not elsewhere. - if (field.final && !field.static) { // A bit hacky to use this as the distinction... - val FormalParameter parameter = compiler.getFormalParameter(field); - if (parameter !== null) { - return appendable; - } - } - return super._generateMember(field, appendable, config); - } -} - diff --git a/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/standalone/CheckDocApplication.java b/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/standalone/CheckDocApplication.java new file mode 100644 index 0000000000..560f3e7908 --- /dev/null +++ b/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/standalone/CheckDocApplication.java @@ -0,0 +1,118 @@ +/******************************************************************************* + * Copyright (c) 2026 Avaloq Group AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Avaloq Group AG - initial API and implementation + *******************************************************************************/ +package com.avaloq.tools.ddk.check.standalone; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.equinox.app.IApplication; +import org.eclipse.equinox.app.IApplicationContext; +import org.eclipse.xtext.resource.XtextResourceSet; + +import com.avaloq.tools.ddk.check.CheckStandaloneSetup; +import com.avaloq.tools.ddk.check.check.CheckCatalog; +import com.avaloq.tools.ddk.check.generator.CheckDocumentationTemplates; +import com.avaloq.tools.ddk.check.generator.CheckGenerator; +import com.google.inject.Injector; +import com.google.inject.Provider; + + +/** + * Eclipse application that emits the full Check documentation tree (HTML pages + * plus the Eclipse-Help {@code toc.xml} / {@code contexts.xml}) from every + * {@code .check} file under a source directory, without requiring an Eclipse + * workbench. + * + * Arguments (positional): {@code } where {@code docsDir} + * is the project's {@code docs/} folder. HTML pages land in + * {@code /content/.html}; the two help-system files land + * directly in {@code /}. + */ +@SuppressWarnings({"nls", "PMD.SystemPrintln"}) +public class CheckDocApplication implements IApplication { + + @Override + public Object start(final IApplicationContext context) throws IOException { + String[] args = (String[]) context.getArguments().get(IApplicationContext.APPLICATION_ARGS); + if (args == null || args.length < 2) { + System.err.println("Usage: -application com.avaloq.tools.ddk.check.core.docApplication "); + return 1; + } + Path sourceDir = Path.of(args[0]).toRealPath(); + Path docsDir = Path.of(args[1]); + Path contentDir = docsDir.resolve("content"); + Files.createDirectories(contentDir); + + Injector injector = new CheckStandaloneSetup().createInjectorAndDoEMFRegistration(); + CheckGenerator generator = injector.getInstance(CheckGenerator.class); + CheckDocumentationTemplates docTemplates = injector.getInstance(CheckDocumentationTemplates.class); + Provider resourceSets = injector.getProvider(XtextResourceSet.class); + XtextResourceSet resourceSet = resourceSets.get(); + + List checkFiles = new ArrayList<>(); + try (Stream walk = Files.walk(sourceDir)) { + walk.filter(p -> p.toString().endsWith(".check")) + .filter(CheckDocApplication::isUserSource) + .forEach(checkFiles::add); + } + + List catalogs = new ArrayList<>(); + for (Path checkFile : checkFiles) { + Resource resource = resourceSet.getResource(URI.createFileURI(checkFile.toAbsolutePath().toString()), true); + for (EObject root : resource.getContents()) { + if (root instanceof CheckCatalog catalog) { + catalogs.add(catalog); + Path target = contentDir.resolve(catalog.getName() + ".html"); + Files.writeString(target, generator.compileDoc(catalog).toString()); + System.out.println("Wrote " + target); + } + } + } + + if (!catalogs.isEmpty()) { + Path toc = docsDir.resolve("toc.xml"); + Files.writeString(toc, docTemplates.compileToc(catalogs).toString()); + System.out.println("Wrote " + toc); + Path contexts = docsDir.resolve("contexts.xml"); + Files.writeString(contexts, docTemplates.compileContexts(catalogs).toString()); + System.out.println("Wrote " + contexts); + Path index = docsDir.resolve("index.html"); + Files.writeString(index, docTemplates.compileIndex(catalogs).toString()); + System.out.println("Wrote " + index); + } + + System.out.println("Processed " + checkFiles.size() + " .check files (" + catalogs.size() + " catalogs)"); + return IApplication.EXIT_OK; + } + + /** True iff {@code p} contains no path segment named {@code target} or starting with a dot. */ + private static boolean isUserSource(final Path p) { + for (Path segment : p) { + String name = segment.toString(); + if ("target".equals(name) || name.startsWith(".")) { + return false; + } + } + return true; + } + + @Override + public void stop() { + // no-op + } +} diff --git a/com.avaloq.tools.ddk.check.runtime.ui/build.properties b/com.avaloq.tools.ddk.check.runtime.ui/build.properties index 688aa65996..404b564d5f 100644 --- a/com.avaloq.tools.ddk.check.runtime.ui/build.properties +++ b/com.avaloq.tools.ddk.check.runtime.ui/build.properties @@ -2,5 +2,4 @@ source.. = src/ bin.includes = META-INF/,\ .,\ plugin.xml,\ - toc.xml,\ - css/ + toc.xml diff --git a/com.avaloq.tools.ddk.check.runtime.ui/css/check.css b/com.avaloq.tools.ddk.check.runtime.ui/css/check.css deleted file mode 100644 index dc58291782..0000000000 --- a/com.avaloq.tools.ddk.check.runtime.ui/css/check.css +++ /dev/null @@ -1,53 +0,0 @@ -body { - font-size: 11pt; - font-family: arial; - font-variant: normal; - margin-left: 1.0cm; -} - -h1 { - font-size: 18pt; - font-weight: bolder; -} - -h2 { - font-size: 14pt; - font-weight: bold; -} - -div#checklabel { - font-size: 12pt; - font-weight: bold; -} - -div.description { - margin-left: 0.5cm; - font-size: 11pt; - border-top: solid thin black; -} - -div.category { - border-top: solid 1px #000; -} - -dt { - font-weight: normal; - font-style: italic; -} - -dd { - margin-top: 2mm; -} - -dl { - margin-top: 3mm; - margin-bottom: 0mm; -} - -div#checkdescription { - margin-left: 1cm; -} - -h3 span.thin { - font-weight: normal; -} \ No newline at end of file diff --git a/com.avaloq.tools.ddk.check.test.runtime/docs/content/ExecutionEnvironment.html b/com.avaloq.tools.ddk.check.test.runtime/docs/content/ExecutionEnvironment.html index 3439e97b51..ea4a27aaaa 100644 --- a/com.avaloq.tools.ddk.check.test.runtime/docs/content/ExecutionEnvironment.html +++ b/com.avaloq.tools.ddk.check.test.runtime/docs/content/ExecutionEnvironment.html @@ -1,22 +1,168 @@ - - + + - - + + ExecutionEnvironment + - -

Check Catalog ExecutionEnvironment

-
-

Test category

- Checks the greeting name length -
-

Greeting name length (error)

- Checks the greeting name length -

Message: Greeting name ...

-
-
+
+

ExecutionEnvironment

+ +
+
+
+

Test category

+

Checks the greeting name length

+
+
+

Greeting name length #

+ error +
+ Checks the greeting name length +
Greeting name ...
+
+
+
- diff --git a/com.avaloq.tools.ddk.check.test.runtime/docs/content/LibraryChecks.html b/com.avaloq.tools.ddk.check.test.runtime/docs/content/LibraryChecks.html index bcf5eaa3b9..a84e860e46 100644 --- a/com.avaloq.tools.ddk.check.test.runtime/docs/content/LibraryChecks.html +++ b/com.avaloq.tools.ddk.check.test.runtime/docs/content/LibraryChecks.html @@ -1,42 +1,198 @@ - - + + - - + + LibraryChecks + - -

Check Catalog LibraryChecks

-

Check catalog for com.avaloq.tools.ddk.check.TestLanguage

-
-

Checks on injections in check catalogs.

- Warning to indicate that this catalog is active. -
-

Check catalog is active (warning)

- Warning to indicate that this catalog is active. -

Message: Catalog is active

-
-
-

Cache injection failed (error)

- Error if the injection didn't work. -

Message: Cache was not injected

-
-
-

Cache doesn't work (error)

- Error if values cannot be read from cache. -

Message: ...

-
-
-
-

Checks on formal parameters

- Test formal parameter access. -
-

Formal Parameters (error)

- Test formal parameter access. -

Message: ...

-
-
+
+

LibraryChecks

+

Check catalog for com.avaloq.tools.ddk.check.TestLanguage

+ +
+
+
+

Checks on injections in check catalogs.

+

Warning to indicate that this catalog is active.

+
+
+

Check catalog is active #

+ warning +
+ Warning to indicate that this catalog is active. +
Catalog is active
+
+
+
+

Cache injection failed #

+ error +
+ Error if the injection didn't work. +
Cache was not injected
+
+
+
+

Cache doesn't work #

+ error +
+ Error if values cannot be read from cache. +
...
+
+
+
+

Checks on formal parameters

+

Test formal parameter access.

+
+
+

Formal Parameters #

+ error +
+ Test formal parameter access. +
...
+
+
+
- diff --git a/com.avaloq.tools.ddk.check.test.runtime/docs/contexts.xml b/com.avaloq.tools.ddk.check.test.runtime/docs/contexts.xml index f88e0238ba..9c88fab9f7 100644 --- a/com.avaloq.tools.ddk.check.test.runtime/docs/contexts.xml +++ b/com.avaloq.tools.ddk.check.test.runtime/docs/contexts.xml @@ -1,18 +1,18 @@ - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + diff --git a/com.avaloq.tools.ddk.check.test.runtime/docs/index.html b/com.avaloq.tools.ddk.check.test.runtime/docs/index.html new file mode 100644 index 0000000000..559ea01ded --- /dev/null +++ b/com.avaloq.tools.ddk.check.test.runtime/docs/index.html @@ -0,0 +1,160 @@ + + + + + + Check Catalogs + + + +
+

Check Catalogs

+

2 catalogs documented.

+
+
+ +
+ + diff --git a/com.avaloq.tools.ddk.check.test.runtime/docs/toc.xml b/com.avaloq.tools.ddk.check.test.runtime/docs/toc.xml index 1ac3dd2a3e..8f38986abc 100644 --- a/com.avaloq.tools.ddk.check.test.runtime/docs/toc.xml +++ b/com.avaloq.tools.ddk.check.test.runtime/docs/toc.xml @@ -1,23 +1,23 @@ - - - + + + - - - - - - - + + - - - + + + + + + + + - \ No newline at end of file + diff --git a/com.avaloq.tools.ddk.check.test.runtime/pom.xml b/com.avaloq.tools.ddk.check.test.runtime/pom.xml index 92efbd8cb5..b6aae47595 100644 --- a/com.avaloq.tools.ddk.check.test.runtime/pom.xml +++ b/com.avaloq.tools.ddk.check.test.runtime/pom.xml @@ -10,4 +10,92 @@ com.avaloq.tools.ddk.check.test.runtime eclipse-plugin - \ No newline at end of file + + + + + generateCheckDocs + + + com.avaloq.tools.ddk + ddk-repository + 17.3.0-SNAPSHOT + eclipse-repository + + + + + + org.eclipse.tycho + tycho-eclipse-plugin + ${tycho.version} + + + generate-check-docs + generate-resources + + eclipse-run + + + JavaSE-21 + + -application + com.avaloq.tools.ddk.check.core.docApplication + ${project.basedir}/src + ${project.basedir}/docs + + + + reactor-p2 + p2 + file:${project.basedir}/../ddk-repository/target/repository/ + + + eclipse-release + p2 + https://download.eclipse.org/releases/2026-03/ + + + eclipse-emf + p2 + https://download.eclipse.org/modeling/emf/emf/builds/release/2.39.0/ + + + eclipse-xtext + p2 + https://download.eclipse.org/modeling/tmf/xtext/updates/releases/2.43.0/ + + + eclipse-mwe + p2 + https://download.eclipse.org/modeling/emft/mwe/updates/releases/2.25.0/ + + + eclipse-orbit + p2 + https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/release/4.39.0 + + + + + com.avaloq.tools.ddk.check.core + eclipse-plugin + + + + + + + + + + + diff --git a/com.avaloq.tools.ddk.sample.helloworld/docs/content/ExecutionEnvironment.html b/com.avaloq.tools.ddk.sample.helloworld/docs/content/ExecutionEnvironment.html new file mode 100644 index 0000000000..2a754921cf --- /dev/null +++ b/com.avaloq.tools.ddk.sample.helloworld/docs/content/ExecutionEnvironment.html @@ -0,0 +1,171 @@ + + + + + + ExecutionEnvironment + + + +
+ ← All catalogs +

ExecutionEnvironment

+ +
+
+
+

Test category

+

Checks the greeting name length

+
+
+

Greeting name length #

+ error +
+ Checks the greeting name length +
Greeting name ...
+
+
+
+ + diff --git a/com.avaloq.tools.ddk.sample.helloworld/docs/content/LibraryChecks.html b/com.avaloq.tools.ddk.sample.helloworld/docs/content/LibraryChecks.html new file mode 100644 index 0000000000..a308f02e0f --- /dev/null +++ b/com.avaloq.tools.ddk.sample.helloworld/docs/content/LibraryChecks.html @@ -0,0 +1,200 @@ + + + + + + LibraryChecks + + + +
+ ← All catalogs +

LibraryChecks

+ +
+
+
+

Checks on injections in check catalogs.

+

Warning to indicate that this catalog is active.

+
+
+

Check catalog is active #

+ warning +
+ Warning to indicate that this catalog is active. +
Catalog is active
+
+
+
+

Cache injection failed #

+ error +
+ Error if the injection didn't work. +
Cache was not injected
+
+
+
+

Cache doesn't work #

+ error +
+ Error if values cannot be read from cache. +
...
+
+
+
+

Checks on formal parameters

+

Test formal parameter access.

+
+
+

Formal Parameters #

+ error +
+ Test formal parameter access. +
...
+
+
+
+ + diff --git a/com.avaloq.tools.ddk.sample.helloworld/docs/contexts.xml b/com.avaloq.tools.ddk.sample.helloworld/docs/contexts.xml index 6d50906410..9c88fab9f7 100644 --- a/com.avaloq.tools.ddk.sample.helloworld/docs/contexts.xml +++ b/com.avaloq.tools.ddk.sample.helloworld/docs/contexts.xml @@ -1,3 +1,18 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + diff --git a/com.avaloq.tools.ddk.sample.helloworld/docs/index.html b/com.avaloq.tools.ddk.sample.helloworld/docs/index.html new file mode 100644 index 0000000000..1123fc36b0 --- /dev/null +++ b/com.avaloq.tools.ddk.sample.helloworld/docs/index.html @@ -0,0 +1,161 @@ + + + + + + Check Catalogs + + + +
+

Check Catalogs

+

2 catalogs documented.

+
+
+ +
+ + diff --git a/com.avaloq.tools.ddk.sample.helloworld/docs/toc.xml b/com.avaloq.tools.ddk.sample.helloworld/docs/toc.xml index 62d47779b1..8f38986abc 100644 --- a/com.avaloq.tools.ddk.sample.helloworld/docs/toc.xml +++ b/com.avaloq.tools.ddk.sample.helloworld/docs/toc.xml @@ -1,3 +1,23 @@ - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + diff --git a/com.avaloq.tools.ddk.sample.helloworld/pom.xml b/com.avaloq.tools.ddk.sample.helloworld/pom.xml index 3a11534241..71d6d3701b 100644 --- a/com.avaloq.tools.ddk.sample.helloworld/pom.xml +++ b/com.avaloq.tools.ddk.sample.helloworld/pom.xml @@ -10,4 +10,100 @@ com.avaloq.tools.ddk com.avaloq.tools.ddk.sample.helloworld eclipse-plugin + + + + + generateCheckDocs + + + + com.avaloq.tools.ddk + ddk-repository + 17.3.0-SNAPSHOT + eclipse-repository + + + + + + org.eclipse.tycho + tycho-eclipse-plugin + ${tycho.version} + + + generate-check-docs + generate-resources + + eclipse-run + + + JavaSE-21 + + -application + com.avaloq.tools.ddk.check.core.docApplication + ${project.basedir}/src + ${project.basedir}/docs + + + + + reactor-p2 + p2 + file:${project.basedir}/../ddk-repository/target/repository/ + + + + eclipse-release + p2 + https://download.eclipse.org/releases/2026-03/ + + + eclipse-emf + p2 + https://download.eclipse.org/modeling/emf/emf/builds/release/2.39.0/ + + + eclipse-xtext + p2 + https://download.eclipse.org/modeling/tmf/xtext/updates/releases/2.43.0/ + + + eclipse-mwe + p2 + https://download.eclipse.org/modeling/emft/mwe/updates/releases/2.25.0/ + + + eclipse-orbit + p2 + https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/release/4.39.0 + + + + + com.avaloq.tools.ddk.check.core + eclipse-plugin + + + + + + + + + + diff --git a/docs/check-doc-generation.md b/docs/check-doc-generation.md new file mode 100644 index 0000000000..98da950f8d --- /dev/null +++ b/docs/check-doc-generation.md @@ -0,0 +1,86 @@ +# Generating Check catalog documentation + +Generate browsable HTML docs for your Check catalogs by running a headless application against your bundle's `.check` sources. The application emits: + +- `docs/index.html` — landing page listing every catalog (self-contained, opens in any browser). +- `docs/content/.html` — one styled page per catalog with severity badges and deep-link anchors. +- `docs/toc.xml`, `docs/contexts.xml` — Eclipse Help integration artifacts (skip if you only need the browser pages). + +The styling is inline; no external CSS, JS or images. Dark mode follows the OS preference. + +## Prerequisites + +A Tycho-based project with a target platform file. Plain-Maven projects are not supported — the generator runs inside an Equinox runtime started by `tycho-eclipse-plugin:eclipse-run`. + +## 1. Add dsl-devkit to your target platform + +In your `.target` file, add the dsl-devkit p2 update site as a location and include `com.avaloq.tools.ddk.check.core`: + +```xml + + + + +``` + +For snapshots, use `https://dsldevkit.github.io/dsl-devkit/p2/snapshots/latest/` instead. To pin to a specific version, use `p2/releases//` or `p2/snapshots//`. + +## 2. Add the profile to the consumer pom + +In the bundle whose `.check` sources you want documented: + +```xml + + + generateCheckDocs + + + + org.eclipse.tycho + tycho-eclipse-plugin + + + generate-check-docs + generate-resources + eclipse-run + + JavaSE-21 + + -application + com.avaloq.tools.ddk.check.core.docApplication + ${project.basedir}/src + ${project.basedir}/docs + + + + com.avaloq.tools.ddk.check.core + eclipse-plugin + + + + + + + + + + +``` + +The application takes two args: `` (walked recursively for `*.check` files) and `` (written into). + +No `` block is needed — `eclipse-run` resolves against your project's target platform. + +## 3. Invoke + +```bash +mvn -PgenerateCheckDocs -DskipTests package +``` + +Then open `docs/index.html` in any browser. + +## Troubleshooting + +- **"Cannot resolve unit com.avaloq.tools.ddk.check.core"** — the dsl-devkit p2 site is not in your target platform. Re-resolve the target after adding it. +- **"No catalogs found"** — `` does not contain any `.check` files. The application walks the directory recursively; check the path you passed as the first ``. +- **"Application com.avaloq.tools.ddk.check.core.docApplication could not be found"** — the bundle is missing from the `` list of the `eclipse-run` execution (it is *not* enough to have it on the target platform; `eclipse-run` only installs what you list).