diff --git a/README.md b/README.md index 4509abf..a25968d 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,59 @@ The project was relocated from . Als ## Provided APIs +### Resources API + +The Resources API provides a modern, Path-based interface for managing build resources. It separates resource management concerns from logging/messaging functionality and provides better control over file operations during the build process. + +**Key Features:** +- Modern `java.nio.file.Path` instead of `java.io.File` +- Change detection with `hasDelta()` (best effort hint) +- Reliable input/output freshness checking with `isUptodate()` +- Optimized output streams that only update files when content changes +- Support for marking generated/derived files for IDE integration +- Convenient copy operation that respects up-to-date checks +- Relative path conversion via `getPath()` + +**Example Usage:** + +```java +@Inject +private Resources resources; + +public void execute() { + // Convert relative paths to absolute paths + Path source = resources.getPath("src/main/java/Example.java"); + Path target = resources.getPath("target/classes/Example.class"); + + // Smart copy - only copies when target is stale + resources.copy(source, target); + + // Check if file has changed (best effort hint) + if (resources.hasDelta(source)) { + // Process the file + } + + // Reliable check for input/output scenarios + if (!resources.isUptodate(target, source)) { + // Regenerate target from source + } + + // Write to a file with change detection + Path generated = resources.getPath("target/generated/Proto.java"); + try (OutputStream out = resources.newOutputStream(generated, true)) { + // Write content - file marked as derived for IDE warnings + out.write(content); + } + + // Mark a file as generated/derived + resources.markDerived(generated); +} +``` + +**When to use `hasDelta()` vs `isUptodate()`:** +- Use `hasDelta()` as a hint for user-editable source files where you want to detect changes +- Use `isUptodate()` when there's a clear input/output relationship, as it handles cases where target files may be deleted or modified outside the build process + ### Messages API The Messages API provides a modern, flexible way to create and manage build messages/markers that inform users in an IDE about issues in their files. It uses a builder pattern for constructing messages in a more convenient and extensible way compared to the legacy BuildContext message methods. diff --git a/src/main/java/org/codehaus/plexus/build/BuildContext.java b/src/main/java/org/codehaus/plexus/build/BuildContext.java index f132223..a6605a7 100644 --- a/src/main/java/org/codehaus/plexus/build/BuildContext.java +++ b/src/main/java/org/codehaus/plexus/build/BuildContext.java @@ -35,7 +35,9 @@ public interface BuildContext { * * @param relpath is path relative to build context basedir * @return a boolean. + * @deprecated Use {@link org.codehaus.plexus.build.resources.Resources#hasDelta(java.nio.file.Path)} instead */ + @Deprecated boolean hasDelta(String relpath); /** @@ -45,7 +47,9 @@ public interface BuildContext { * @since 0.0.5 * @param file a {@link java.io.File} object. * @return a boolean. + * @deprecated Use {@link org.codehaus.plexus.build.resources.Resources#hasDelta(java.nio.file.Path)} instead */ + @Deprecated boolean hasDelta(File file); /** @@ -54,7 +58,9 @@ public interface BuildContext { * * @param relpaths paths relative to build context basedir * @return a boolean. + * @deprecated Use {@link org.codehaus.plexus.build.resources.Resources#hasDelta(java.nio.file.Path)} instead */ + @Deprecated boolean hasDelta(List relpaths); /** @@ -62,7 +68,9 @@ public interface BuildContext { * * @see #newFileOutputStream(File) * @param file a {@link java.io.File} object. + * @deprecated Use {@link org.codehaus.plexus.build.resources.Resources#refresh(java.nio.file.Path)} instead */ + @Deprecated void refresh(File file); /** @@ -78,7 +86,9 @@ public interface BuildContext { * @param file a {@link java.io.File} object. * @return a {@link java.io.OutputStream} object. * @throws java.io.IOException if any. + * @deprecated Use {@link org.codehaus.plexus.build.resources.Resources#newOutputStream(java.nio.file.Path)} instead */ + @Deprecated OutputStream newFileOutputStream(File file) throws IOException; /** @@ -236,6 +246,8 @@ public interface BuildContext { * @param target a {@link java.io.File} object. * @param source a {@link java.io.File} object. * @return a boolean. + * @deprecated Use {@link org.codehaus.plexus.build.resources.Resources#isUptodate(java.nio.file.Path, java.nio.file.Path)} instead */ + @Deprecated boolean isUptodate(File target, File source); } diff --git a/src/main/java/org/codehaus/plexus/build/resources/DefaultResources.java b/src/main/java/org/codehaus/plexus/build/resources/DefaultResources.java new file mode 100644 index 0000000..6f96f55 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/build/resources/DefaultResources.java @@ -0,0 +1,154 @@ +/* +Copyright (c) 2008 Sonatype, Inc. All rights reserved. + +This program is licensed to you under the Apache License Version 2.0, +and you may not use this file except in compliance with the Apache License Version 2.0. +You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + +Unless required by applicable law or agreed to in writing, +software distributed under the Apache License Version 2.0 is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. +*/ +package org.codehaus.plexus.build.resources; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.maven.plugin.LegacySupport; +import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.build.BuildContext; + +/** + * Default implementation of the Resources interface. + *

+ * This implementation delegates to the BuildContext for compatibility with existing + * build infrastructure. It provides a transition path from the File-based API to the + * modern Path-based API. + *

+ */ +@Named("default") +@Singleton +public class DefaultResources implements Resources { + + private final BuildContext buildContext; + private final LegacySupport legacySupport; + + /** + * Creates a new DefaultResources instance. + * + * @param buildContext the BuildContext to which operations will be delegated + * @param legacySupport the LegacySupport to get the current Maven project + */ + @Inject + public DefaultResources(BuildContext buildContext, LegacySupport legacySupport) { + this.buildContext = buildContext; + this.legacySupport = legacySupport; + } + + @Override + public boolean hasDelta(Path file) { + if (file == null) { + return false; + } + return buildContext.hasDelta(file.toFile()); + } + + @Override + public void refresh(Path file) { + if (file != null) { + buildContext.refresh(file.toFile()); + } + } + + @Override + public OutputStream newOutputStream(Path file) throws IOException { + if (file == null) { + throw new IllegalArgumentException("file cannot be null"); + } + return buildContext.newFileOutputStream(file.toFile()); + } + + @Override + public OutputStream newOutputStream(Path file, boolean derived) throws IOException { + OutputStream outputStream = newOutputStream(file); + if (derived) { + // Mark the file as derived after creating the output stream. + // In the default implementation, markDerived is a no-op, so the timing doesn't matter. + // Custom implementations that track derived files should consider wrapping the stream + // to defer marking until successful close() to avoid marking files on failed writes. + markDerived(file); + } + return outputStream; + } + + @Override + public boolean isUptodate(Path target, Path source) { + if (target == null || source == null) { + return false; + } + return buildContext.isUptodate(target.toFile(), source.toFile()); + } + + @Override + public Path getPath(String relpath) { + if (relpath == null) { + throw new IllegalArgumentException("relpath cannot be null"); + } + // Get the basedir from the current Maven project via LegacySupport + MavenProject project = + legacySupport.getSession() != null ? legacySupport.getSession().getCurrentProject() : null; + if (project != null) { + File basedir = project.getBasedir(); + if (basedir != null) { + return basedir.toPath().resolve(relpath); + } + } + // Fallback to current working directory if project is not available + return java.nio.file.Paths.get(relpath); + } + + @Override + public void markDerived(Path file) { + // No-op in the default implementation + // Custom implementations (e.g., IDE integrations) can override this + // to provide actual derived file tracking + } + + @Override + public void copy(Path source, Path target) throws IOException { + if (source == null) { + throw new IllegalArgumentException("source cannot be null"); + } + if (target == null) { + throw new IllegalArgumentException("target cannot be null"); + } + + // Only copy if the target is not up-to-date with the source + if (!isUptodate(target, source)) { + // Ensure parent directory exists + Path parentDir = target.getParent(); + if (parentDir != null && !Files.exists(parentDir)) { + Files.createDirectories(parentDir); + } + + // Copy using Files API and newOutputStream to ensure proper change detection + try (InputStream in = Files.newInputStream(source); + OutputStream out = newOutputStream(target)) { + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + } + } + } + } +} diff --git a/src/main/java/org/codehaus/plexus/build/resources/Resources.java b/src/main/java/org/codehaus/plexus/build/resources/Resources.java new file mode 100644 index 0000000..73a3cb0 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/build/resources/Resources.java @@ -0,0 +1,184 @@ +/* +Copyright (c) 2008 Sonatype, Inc. All rights reserved. + +This program is licensed to you under the Apache License Version 2.0, +and you may not use this file except in compliance with the Apache License Version 2.0. +You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + +Unless required by applicable law or agreed to in writing, +software distributed under the Apache License Version 2.0 is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. +*/ +package org.codehaus.plexus.build.resources; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Path; + +/** + *

Resources interface.

+ *

+ * This API provides a modern, flexible way to manage build resources using Path-based methods. + * It separates resource management concerns from logging/messaging functionality and provides + * better control over file operations during the build process. + *

+ *

+ * Example usage: + *

+ *
+ * // Convert relative path to absolute Path
+ * Path sourceFile = resources.getPath("src/main/java/Example.java");
+ * Path targetFile = resources.getPath("target/classes/Example.class");
+ *
+ * // Copy source to target only if needed
+ * resources.copy(sourceFile, targetFile);
+ *
+ * // Mark generated file as derived
+ * resources.markDerived(targetFile);
+ * 
+ */ +public interface Resources { + + /** + * Returns true if the file has changed since last build. + *

+ * Important: This is a "best effort" hint and should primarily be used + * for user-editable source files where you want to detect changes. For generated + * files or when there is a clear input/output relationship, prefer using + * {@link #isUptodate(Path, Path)} instead, as it handles cases where target files + * may be deleted or modified outside of the build process. + *

+ * + * @param file the file to check for changes + * @return true if the file has changed or if the file is not under basedir + */ + boolean hasDelta(Path file); + + /** + * Indicates that the file or folder content has been modified during the build. + *

+ * This method should be called when file content is modified through means other + * than {@link #newOutputStream(Path)} or {@link #newOutputStream(Path, boolean)}. + * Files changed using OutputStreams returned by those methods do not need to be + * explicitly refreshed. + *

+ * + * @param file the file that was modified + */ + void refresh(Path file); + + /** + * Returns a new OutputStream that writes to the specified file. + *

+ * Files changed using OutputStream returned by this method do not need to be + * explicitly refreshed using {@link #refresh(Path)}. + *

+ *

+ * As an optimization, some implementations may attempt to avoid writing to the file + * if the file content has not changed. This ensures that file timestamps are only + * updated when actual changes occur, which is important for incremental build performance. + * This optimization is implementation-dependent and may not apply to all implementations. + *

+ * + * @param file the file to write to + * @return an OutputStream for writing to the file + * @throws IOException if an I/O error occurs + */ + OutputStream newOutputStream(Path file) throws IOException; + + /** + * Returns a new OutputStream that writes to the specified file and optionally + * marks it as derived. + *

+ * Files changed using OutputStream returned by this method do not need to be + * explicitly refreshed using {@link #refresh(Path)}. + *

+ *

+ * As an optimization, some implementations may attempt to avoid writing to the file + * if the file content has not changed. This optimization is implementation-dependent + * and may not apply to all implementations. + *

+ *

+ * When derived is true, the file is marked as generated/derived, + * which can be used by IDEs to warn users if they attempt to edit the file. + * This is useful for code generators that will overwrite any manual changes on + * subsequent builds. + *

+ * + * @param file the file to write to + * @param derived true to mark the file as derived/generated + * @return an OutputStream for writing to the file + * @throws IOException if an I/O error occurs + */ + OutputStream newOutputStream(Path file, boolean derived) throws IOException; + + /** + * Returns true if the target file exists and is up-to-date compared + * to the source file. + *

+ * More specifically, this method returns true when both target and + * source files exist, do not have changes since last incremental build, and the + * target file was last modified later than the source file. Returns false + * in all other cases. + *

+ *

+ * This method should be preferred over {@link #hasDelta(Path)} when there is a + * clear input/output relationship between files, as it properly handles cases + * where the target file may be deleted or modified outside of the build process. + *

+ * + * @param target the target/output file + * @param source the source/input file + * @return true if the target is up-to-date with the source + */ + boolean isUptodate(Path target, Path source); + + /** + * Converts a relative path string to an absolute Path based on the build context basedir. + *

+ * This method is useful for converting relative paths (e.g., "src/main/java/Example.java") + * to absolute Paths that can be used with other methods in this interface. + *

+ * + * @param relpath the relative path string + * @return the absolute Path resolved against the build context basedir + */ + Path getPath(String relpath); + + /** + * Marks a file as derived (generated) by the build process. + *

+ * This is useful for code generators and other build plugins that create files + * that should not be edited manually. IDEs can use this information to warn + * users if they attempt to modify derived files, as such changes will typically + * be overwritten on the next build. + *

+ *

+ * Note: In the default implementation, this is a no-op. Custom implementations + * (e.g., IDE integrations) may provide actual derived file tracking. + *

+ * + * @param file the file to mark as derived + */ + void markDerived(Path file); + + /** + * Copies the source file to the target file if the target is not up-to-date. + *

+ * This is a convenience method that combines {@link #isUptodate(Path, Path)} + * checking with file copying using {@link #newOutputStream(Path)}. The copy + * only occurs when the target does not exist or is not up-to-date with the source. + *

+ *

+ * The implementation uses the Files API to open an input stream from the source + * and copies it to an output stream obtained from {@link #newOutputStream(Path)}, + * ensuring that the target's timestamp is only updated if the content actually changes. + *

+ * + * @param source the source file to copy from + * @param target the target file to copy to + * @throws IOException if an I/O error occurs during the copy operation + */ + void copy(Path source, Path target) throws IOException; +}