Collect output file state 80/104180/10
authorRobert Varga <robert.varga@pantheon.tech>
Thu, 26 Jan 2023 18:36:57 +0000 (19:36 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Fri, 24 Feb 2023 19:46:46 +0000 (20:46 +0100)
We need to capture the state of files we are emitting. Plug into
GeneratorTask and generate the file name and has as part of file
writeout.

JIRA: YANGTOOLS-1166
Change-Id: I9e0422e5d1bb7be218ca3dbc9e7611395554a3e1
Signed-off-by: Ruslan Kashapov <ruslan.kashapov@pantheon.tech>
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
plugin/yang-maven-plugin/src/main/java/org/opendaylight/yangtools/yang2sources/plugin/CapturingOutputStream.java [new file with mode: 0644]
plugin/yang-maven-plugin/src/main/java/org/opendaylight/yangtools/yang2sources/plugin/FileState.java [new file with mode: 0644]
plugin/yang-maven-plugin/src/main/java/org/opendaylight/yangtools/yang2sources/plugin/GeneratorTask.java
plugin/yang-maven-plugin/src/main/java/org/opendaylight/yangtools/yang2sources/plugin/YangToSourcesProcessor.java
plugin/yang-maven-plugin/src/main/java/org/opendaylight/yangtools/yang2sources/plugin/YangToSourcesState.java [new file with mode: 0644]

diff --git a/plugin/yang-maven-plugin/src/main/java/org/opendaylight/yangtools/yang2sources/plugin/CapturingOutputStream.java b/plugin/yang-maven-plugin/src/main/java/org/opendaylight/yangtools/yang2sources/plugin/CapturingOutputStream.java
new file mode 100644 (file)
index 0000000..c66283d
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech, s.r.o. 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
+ */
+package org.opendaylight.yangtools.yang2sources.plugin;
+
+import com.google.common.hash.Hashing;
+import com.google.common.hash.HashingOutputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * An {@link OutputStream} which captures the sum of its contents.
+ */
+final class CapturingOutputStream extends FilterOutputStream {
+    private long size;
+
+    CapturingOutputStream(final OutputStream out) {
+        super(new HashingOutputStream(Hashing.crc32c(), out));
+    }
+
+    @Override
+    @SuppressWarnings("checkstyle:parameterName")
+    public void write(final int b) throws IOException {
+        super.write(b);
+        size++;
+    }
+
+    @Override
+    public void write(final byte[] bytes, final int off, final int len) throws IOException {
+        super.write(bytes, off, len);
+        size += len;
+    }
+
+    long size() {
+        return size;
+    }
+
+    int crc32c() {
+        return ((HashingOutputStream) out).hash().asInt();
+    }
+}
diff --git a/plugin/yang-maven-plugin/src/main/java/org/opendaylight/yangtools/yang2sources/plugin/FileState.java b/plugin/yang-maven-plugin/src/main/java/org/opendaylight/yangtools/yang2sources/plugin/FileState.java
new file mode 100644 (file)
index 0000000..0be5826
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech, s.r.o. 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
+ */
+package org.opendaylight.yangtools.yang2sources.plugin;
+
+import static java.util.Objects.requireNonNull;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.nio.file.attribute.BasicFileAttributes;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.yangtools.concepts.WritableObject;
+import org.opendaylight.yangtools.concepts.WritableObjects;
+
+/**
+ * Hash of a single file state. {@link #size()} corresponds to {@link BasicFileAttributes#size()}.
+ */
+record FileState(@NonNull String path, long size, int crc32) implements WritableObject {
+    FileState {
+        requireNonNull(path);
+    }
+
+    public static FileState read(final DataInput in) throws IOException {
+        return new FileState(in.readUTF(), WritableObjects.readLong(in), in.readInt());
+    }
+
+    @Override
+    public void writeTo(final DataOutput out) throws IOException {
+        out.writeUTF(path);
+        WritableObjects.writeLong(out, size);
+        out.writeInt(crc32);
+    }
+}
index 0c87465acc1d0a32fad599f616eb08afddfda39d..43f2d7382a416833ac292d87e4d0efe4cee31af4 100644 (file)
@@ -17,9 +17,7 @@ import com.google.common.collect.Table;
 import com.google.common.collect.Table.Cell;
 import java.io.File;
 import java.io.IOException;
-import java.io.OutputStream;
 import java.nio.file.Files;
-import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -63,7 +61,7 @@ final class GeneratorTask extends ParserConfigAware {
         return factory.parserConfig();
     }
 
-    Collection<File> execute(final BuildContext buildContext) throws FileGeneratorException, IOException {
+    List<FileState> execute(final BuildContext buildContext) throws FileGeneratorException, IOException {
         // Step one: determine what files are going to be generated
         final Stopwatch sw = Stopwatch.createStarted();
         final FileGenerator gen = factory.generator();
@@ -110,7 +108,7 @@ final class GeneratorTask extends ParserConfigAware {
 
         // Step four: submit all code generation tasks (via parallelStream()) and wait for them to complete
         sw.reset().start();
-        final List<File> result = dirs.values().parallelStream()
+        final var result = dirs.values().parallelStream()
                 .map(WriteTask::generateFile)
                 .collect(Collectors.toList());
         LOG.debug("Generated {} files in {}", result.size(), sw);
@@ -190,13 +188,13 @@ final class GeneratorTask extends ParserConfigAware {
             this.file = requireNonNull(file);
         }
 
-        File generateFile() {
-            try (OutputStream stream = buildContext.newFileOutputStream(target)) {
-                file.writeBody(stream);
+        FileState generateFile() {
+            try (var out = new CapturingOutputStream(buildContext.newFileOutputStream(target))) {
+                file.writeBody(out);
+                return new FileState(target.getPath(), out.size(), out.crc32c());
             } catch (IOException e) {
                 throw new IllegalStateException("Failed to generate file " + target, e);
             }
-            return target;
         }
     }
 }
index f180634968d4e477b9c77e5fa9962a83740095a7..d06acc6ad0267b372ca6e55781e3f574c73407af 100644 (file)
@@ -14,8 +14,8 @@ import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Stopwatch;
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ImmutableSet.Builder;
 import com.google.common.collect.Maps;
 import java.io.File;
 import java.io.IOException;
@@ -24,6 +24,7 @@ import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -64,6 +65,7 @@ class YangToSourcesProcessor {
     static final String LOG_PREFIX = "yang-to-sources:";
     private static final String META_INF_STR = "META-INF";
     private static final String YANG_STR = "yang";
+    private static final String BUILD_CONTEXT_STATE_NAME = YangToSourcesProcessor.class.getName();
 
     static final String META_INF_YANG_STRING = META_INF_STR + File.separator + YANG_STR;
     static final String META_INF_YANG_STRING_JAR = META_INF_STR + "/" + YANG_STR;
@@ -112,6 +114,12 @@ class YangToSourcesProcessor {
     }
 
     void conditionalExecute(final boolean skip) throws MojoExecutionException, MojoFailureException {
+        var prevState = buildContext.getValue(BUILD_CONTEXT_STATE_NAME);
+        if (prevState == null) {
+            LOG.debug("{} BuildContext did not provide state", LOG_PREFIX);
+            // FIXME: look for persisted state and restore it
+        }
+
         /*
          * Collect all files which affect YANG context. This includes all
          * files in current project and optionally any jars/files in the
@@ -186,8 +194,8 @@ class YangToSourcesProcessor {
         LOG.debug("Found project files: {}", yangFilesInProject);
         LOG.info("{} Project model files found: {} in {}", LOG_PREFIX, yangFilesInProject.size(), watch);
 
-        // FIXME: store these files into state, so that we can verify/clean up
-        final Builder<File> files = ImmutableSet.builder();
+        final var outputFiles = ImmutableList.<FileState>builder();
+
         for (YangParserConfiguration parserConfig : parserConfigs) {
             final Optional<ProcessorModuleReactor> optReactor = createReactor(yangFilesInProject,
                 parserConfig, dependencies, parsed);
@@ -208,7 +216,7 @@ class YangToSourcesProcessor {
 
                     LOG.info("{} {} YANG models processed in {}", LOG_PREFIX, holder.getContext().getModules().size(),
                         sw);
-                    files.addAll(generateSources(holder, codeGenerators, parserConfig));
+                    outputFiles.addAll(generateSources(holder, codeGenerators, parserConfig));
                 } else {
                     LOG.info("{} Skipping YANG code generation because property yang.skip is true", LOG_PREFIX);
                 }
@@ -229,6 +237,22 @@ class YangToSourcesProcessor {
         YangProvider.setResource(generatedServicesDir, project);
         LOG.debug("{} Yang services files from: {} marked as resources: {}", LOG_PREFIX, generatedServicesDir,
             META_INF_YANG_SERVICES_STRING_JAR);
+
+        final var uniqueOutputFiles = new LinkedHashMap<String, FileState>();
+        for (var fileHash : outputFiles.build()) {
+            final var prev = uniqueOutputFiles.putIfAbsent(fileHash.path(), fileHash);
+            if (prev != null) {
+                throw new MojoFailureException("Duplicate files " + prev + " and " + fileHash);
+            }
+        }
+
+        // FIXME: store these files into state, so that we can verify/clean up
+        final var outputState = new YangToSourcesState(ImmutableMap.copyOf(uniqueOutputFiles));
+        buildContext.setValue(BUILD_CONTEXT_STATE_NAME, outputState);
+        if (buildContext.getValue(BUILD_CONTEXT_STATE_NAME) == null) {
+            LOG.debug("{} BuildContext did not retain state, persisting", LOG_PREFIX);
+            // FIXME: persist in target/ directory (there is a maven best practice where)
+        }
     }
 
     private List<GeneratorTaskFactory> instantiateGenerators() throws MojoExecutionException {
@@ -320,9 +344,10 @@ class YangToSourcesProcessor {
     /**
      * Call generate on every generator from plugin configuration.
      */
-    private Set<File> generateSources(final ContextHolder context, final Collection<GeneratorTaskFactory> generators,
-            final YangParserConfiguration parserConfig) throws MojoFailureException {
-        final Builder<File> allFiles = ImmutableSet.builder();
+    private List<FileState> generateSources(final ContextHolder context,
+            final Collection<GeneratorTaskFactory> generators, final YangParserConfiguration parserConfig)
+                throws MojoFailureException {
+        final var generatorToFiles = ImmutableList.<FileState>builder();
         for (GeneratorTaskFactory factory : generators) {
             if (!parserConfig.equals(factory.parserConfig())) {
                 continue;
@@ -332,26 +357,22 @@ class YangToSourcesProcessor {
             final GeneratorTask task = factory.createTask(project, context);
             LOG.debug("{} Task {} initialized in {}", LOG_PREFIX, task, sw);
 
-            final Collection<File> files;
+            final List<FileState> files;
             try {
                 files = task.execute(buildContext);
             } catch (FileGeneratorException | IOException e) {
                 throw new MojoFailureException(LOG_PREFIX + " Generator " + factory + " failed", e);
             }
 
-            LOG.debug("{} Sources generated by {}: {}", LOG_PREFIX, factory.generatorName(), files);
+            final String generatorName = factory.generatorName();
+            LOG.debug("{} Sources generated by {}: {}", LOG_PREFIX, generatorName, files);
 
-            final int fileCount;
-            if (files != null) {
-                fileCount = files.size();
-                allFiles.addAll(files);
-            } else {
-                fileCount = 0;
-            }
+            final int fileCount = files.size();
+            generatorToFiles.addAll(files);
 
-            LOG.info("{} Sources generated by {}: {} in {}", LOG_PREFIX, factory.generatorName(), fileCount, sw);
+            LOG.info("{} Sources generated by {}: {} in {}", LOG_PREFIX, generatorName, fileCount, sw);
         }
 
-        return allFiles.build();
+        return generatorToFiles.build();
     }
 }
diff --git a/plugin/yang-maven-plugin/src/main/java/org/opendaylight/yangtools/yang2sources/plugin/YangToSourcesState.java b/plugin/yang-maven-plugin/src/main/java/org/opendaylight/yangtools/yang2sources/plugin/YangToSourcesState.java
new file mode 100644 (file)
index 0000000..be9bf4d
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech, s.r.o. 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
+ */
+package org.opendaylight.yangtools.yang2sources.plugin;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.function.Function;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.yangtools.concepts.WritableObject;
+
+/**
+ * State of the result of a {@link YangToSourcesMojo} execution run.
+ */
+// FIXME: expand to capture:
+//        - input YANG files
+//        - code generators and their config
+record YangToSourcesState(@NonNull ImmutableMap<String, FileState> outputFiles) implements WritableObject {
+    YangToSourcesState {
+        requireNonNull(outputFiles);
+    }
+
+    static @NonNull YangToSourcesState readFrom(final DataInput in) throws IOException {
+        final ImmutableMap<String, FileState> outputStateMap = readToMap(in, FileState::read, FileState::path);
+        return new YangToSourcesState(outputStateMap);
+
+    }
+
+    @Override
+    public void writeTo(final DataOutput out) throws IOException {
+        write(out, outputFiles.values());
+    }
+
+    private static <T extends WritableObject> void write(final DataOutput out, final Collection<T> items)
+            throws IOException {
+        out.writeInt(items.size());
+        for (var item : items) {
+            // TODO: discover common prefix and serialize it just once -- but that will complicate things a log, as
+            //       we really maintain a hierarchy, which means we want the Map sorted in a certain way.
+            item.writeTo(out);
+        }
+    }
+
+    private static <T extends WritableObject> ImmutableMap<String, T> readToMap(final DataInput in,
+            final DataReader<T> reader, final Function<T, String> keyExtractor) throws IOException {
+        final int size = in.readInt();
+        if (size == 0) {
+            ImmutableMap.of();
+        }
+        final var outputFiles = new ArrayList<T>(size);
+        for (int i = 0; i < size; ++i) {
+            outputFiles.add(reader.read(in));
+        }
+        return Maps.uniqueIndex(outputFiles, keyExtractor::apply);
+    }
+
+    @FunctionalInterface
+    interface DataReader<T extends WritableObject> {
+        T read(DataInput in) throws IOException;
+    }
+}