PreviewGenerator.java
/*******************************************************************************
* Copyright (c) 2026 Carsten Hammer.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Carsten Hammer - initial API and implementation
*******************************************************************************/
package org.sandbox.jdt.triggerpattern.api;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
/**
* Generates before/after preview text from source and replacement patterns.
*
* <p>Automatically substitutes placeholders with example values to produce
* human-readable preview text. Eliminates the need for manually writing
* preview code for each transformation rule.</p>
*
* <h2>Example</h2>
* <pre>
* Source pattern: "new String($bytes, \"UTF-8\")"
* Replacement: "new String($bytes, java.nio.charset.StandardCharsets.UTF_8)"
*
* Generated preview:
* Before: new String(bytes, "UTF-8")
* After: new String(bytes, java.nio.charset.StandardCharsets.UTF_8)
* </pre>
*
* @since 1.3.2
*/
public final class PreviewGenerator {
/**
* Default example values for placeholders by naming convention.
*/
private static final Map<String, String> DEFAULT_EXAMPLES = new HashMap<>();
static {
DEFAULT_EXAMPLES.put("$x", "x"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$y", "y"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$z", "z"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$a", "a"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$b", "b"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$var", "value"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$obj", "obj"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$name", "name"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$type", "MyType"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$T", "String"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$path", "path"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$bytes", "bytes"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$enc", "\"UTF-8\""); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$encoding", "\"UTF-8\""); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$charset", "charset"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$src", "source"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$msg", "message"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$message", "message"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$expected", "expected"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$actual", "actual"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$cond", "condition"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$result", "result"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$list", "list"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$map", "map"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$key", "key"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$value", "value"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$stream", "stream"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$length", "length"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$len", "len"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$size", "size"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$copy", "copy"); //$NON-NLS-1$ //$NON-NLS-2$
DEFAULT_EXAMPLES.put("$srcPos", "0"); //$NON-NLS-1$ //$NON-NLS-2$
}
private PreviewGenerator() {
// Utility class
}
/**
* Generates a preview for a transformation rule.
*
* @param rule the transformation rule
* @return the generated preview
*/
public static Preview generatePreview(TransformationRule rule) {
String sourceText = rule.sourcePattern().getValue();
// Collect all placeholders from the source pattern
Map<String, String> examples = collectPlaceholderExamples(sourceText);
String before = substitutePlaceholders(sourceText, examples);
String after = null;
if (!rule.isHintOnly() && !rule.alternatives().isEmpty()) {
String firstReplacement = rule.alternatives().get(0).replacementPattern();
after = substitutePlaceholders(firstReplacement, examples);
}
return new Preview(before, after, rule.getDescription());
}
/**
* Generates a preview from a source pattern and replacement pattern.
*
* @param sourcePattern the source pattern text
* @param replacementPattern the replacement pattern text (may be {@code null} for hint-only)
* @return the generated preview
*/
public static Preview generatePreview(String sourcePattern, String replacementPattern) {
Map<String, String> examples = collectPlaceholderExamples(sourcePattern);
String before = substitutePlaceholders(sourcePattern, examples);
String after = replacementPattern != null
? substitutePlaceholders(replacementPattern, examples)
: null;
return new Preview(before, after, null);
}
/**
* Collects placeholder names from a pattern and assigns example values.
*
* @param patternText the pattern text
* @return map of placeholder name to example value
*/
static Map<String, String> collectPlaceholderExamples(String patternText) {
Map<String, String> examples = new HashMap<>();
java.util.regex.Pattern phPattern = java.util.regex.Pattern.compile("\\$([a-zA-Z_][a-zA-Z0-9_]*)\\$?"); //$NON-NLS-1$
Matcher matcher = phPattern.matcher(patternText);
int varCounter = 0;
while (matcher.find()) {
String fullPlaceholder = matcher.group(0);
if (!examples.containsKey(fullPlaceholder)) {
String example = DEFAULT_EXAMPLES.get(fullPlaceholder);
if (example == null) {
// Generate a meaningful name from the placeholder
String name = matcher.group(1);
if (fullPlaceholder.endsWith("$")) { //$NON-NLS-1$
// Variadic placeholder: use "arg1, arg2"
example = "arg" + (++varCounter) + ", arg" + (++varCounter); //$NON-NLS-1$ //$NON-NLS-2$
} else {
example = name;
}
}
examples.put(fullPlaceholder, example);
}
}
return examples;
}
/**
* Substitutes placeholders in a pattern with example values.
*
* @param patternText the pattern text with placeholders
* @param examples the placeholder-to-value mapping
* @return the substituted text
*/
static String substitutePlaceholders(String patternText, Map<String, String> examples) {
String result = patternText;
// Sort by length descending to replace longer placeholders first (e.g., $args$ before $a)
List<Map.Entry<String, String>> sorted = new java.util.ArrayList<>(examples.entrySet());
sorted.sort((a, b) -> Integer.compare(b.getKey().length(), a.getKey().length()));
for (Map.Entry<String, String> entry : sorted) {
result = result.replace(entry.getKey(), entry.getValue());
}
return result;
}
/**
* Represents a before/after preview for a transformation rule.
*
* @param before the code before transformation
* @param after the code after transformation (null for hint-only rules)
* @param description optional description of the transformation
*/
public record Preview(String before, String after, String description) {
/**
* Returns {@code true} if this preview has an "after" transformation.
*
* @return {@code true} if not hint-only
*/
public boolean hasTransformation() {
return after != null;
}
/**
* Formats the preview as a human-readable string.
*
* @return formatted preview text
*/
public String format() {
StringBuilder sb = new StringBuilder();
if (description != null) {
sb.append("// ").append(description).append('\n'); //$NON-NLS-1$
}
sb.append("Before: ").append(before); //$NON-NLS-1$
if (after != null) {
sb.append('\n');
sb.append("After: ").append(after); //$NON-NLS-1$
}
return sb.toString();
}
}
}