HintFileCreation.java
/*******************************************************************************
* Copyright (c) 2026 Carsten Hammer.
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.sandbox.jdt.internal.ui.wizard;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.sandbox.jdt.triggerpattern.api.RuleExampleSuite;
/** Creates new rule evidence under one workspace rule; never overwrites a resource. */
final class HintFileCreation {
private HintFileCreation() {
}
static void create(IContainer container, String fileName, String content, RuleExampleSuite suite,
IProgressMonitor monitor) throws CoreException {
String examples = suite == null ? null : suite.toJson();
IFile hint = container.getFile(new Path(fileName));
IFile sidecar = container.getFile(new Path(fileName + ".examples.json")); //$NON-NLS-1$
container.getWorkspace().run(progress -> {
progress.beginTask("Creating " + fileName, examples == null ? 1 : 2); //$NON-NLS-1$
boolean hintCreated = false;
boolean sidecarCreated = false;
try {
checkCanceled(progress);
requireAbsent(hint);
if (examples != null) {
requireAbsent(sidecar);
}
// A separate child monitor keeps resource-internal progress from completing
// this transaction's first step before create() has returned successfully.
hint.create(bytes(content), false, new NullProgressMonitor());
hintCreated = true;
progress.worked(1);
checkCanceled(progress);
if (examples != null) {
sidecar.create(bytes(examples), false, new NullProgressMonitor());
sidecarCreated = true;
progress.worked(1);
checkCanceled(progress);
}
} catch (CoreException | RuntimeException failure) {
// Existing resources never set these flags. Cleanup must also work after
// cancellation, so it deliberately receives a fresh monitor.
if (sidecarCreated) {
rollback(sidecar, failure);
}
if (hintCreated) {
rollback(hint, failure);
}
throw failure;
} finally {
progress.done();
}
}, container, IWorkspace.AVOID_UPDATE, monitor);
}
private static ByteArrayInputStream bytes(String contents) {
return new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8));
}
private static void requireAbsent(IFile file) throws CoreException {
if (file.exists()) {
throw new CoreException(new Status(IStatus.ERROR, "sandbox_triggerpattern", //$NON-NLS-1$
"File already exists: " + file.getFullPath())); //$NON-NLS-1$
}
}
private static void checkCanceled(IProgressMonitor monitor) {
if (monitor.isCanceled() || Thread.currentThread().isInterrupted()) {
throw new OperationCanceledException();
}
}
private static void rollback(IFile file, Exception failure) {
try {
file.delete(true, new NullProgressMonitor());
} catch (CoreException cleanupFailure) {
failure.addSuppressed(cleanupFailure);
}
}
}