--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
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;
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();
// 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);
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;
}
}
}
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;
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;
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;
}
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
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);
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);
}
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 {
/**
* 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;
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();
}
}
--- /dev/null
+/*
+ * 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;
+ }
+}