All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.quarkiverse.roq.data.deployment.RoqDataReaderProcessor Maven / Gradle / Ivy

The newest version!
package io.quarkiverse.roq.data.deployment;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.jboss.jandex.*;
import org.jboss.logging.Logger;

import io.quarkiverse.roq.data.deployment.converters.DataConverterFinder;
import io.quarkiverse.roq.data.deployment.items.DataMappingBuildItem;
import io.quarkiverse.roq.data.deployment.items.RoqDataBuildItem;
import io.quarkiverse.roq.data.deployment.items.RoqDataJsonBuildItem;
import io.quarkiverse.roq.data.runtime.annotations.DataMapping;
import io.quarkiverse.roq.deployment.items.RoqJacksonBuildItem;
import io.quarkiverse.roq.deployment.items.RoqProjectBuildItem;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem;

public class RoqDataReaderProcessor {

    private static final Set SUPPORTED_EXTENSIONS = Set.of(".json", ".yaml", ".yml");
    private static final Logger LOG = Logger.getLogger(RoqDataReaderProcessor.class);
    private static final DotName DATA_MAPPING_ANNOTATION = DotName.createSimple(DataMapping.class.getName());
    RoqDataConfig roqDataConfig;

    @BuildStep
    void scanDataFiles(RoqProjectBuildItem roqProject,
            RoqDataConfig config,
            RoqJacksonBuildItem jackson,
            BuildProducer dataProducer,
            BuildProducer watchedFilesProducer) {
        if (roqProject.isActive()) {
            DataConverterFinder converter = new DataConverterFinder(jackson.getJsonMapper(), jackson.getYamlMapper());
            try {
                Collection items = scanDataFiles(roqProject, converter, watchedFilesProducer, config);

                for (RoqDataBuildItem item : items) {
                    dataProducer.produce(item);
                }

            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

    }

    @BuildStep
    AdditionalIndexedClassesBuildItem addAnnotation() {
        return new AdditionalIndexedClassesBuildItem(DATA_MAPPING_ANNOTATION.toString());
    }

    @BuildStep
    void scanDataMappings(
            CombinedIndexBuildItem index,
            List roqDataBuildItems,
            BuildProducer dataMappingProducer,
            BuildProducer dataJsonProducer,
            RoqDataConfig config) {
        Collection annotations = index.getIndex().getAnnotations(DATA_MAPPING_ANNOTATION);

        Map dataJsonMap = roqDataBuildItems.stream()
                .collect(Collectors.toMap(RoqDataBuildItem::getName, Function.identity()));

        Map annotationMap = annotations.stream().collect(Collectors.toMap(
                annotation -> annotation.value().asString(), Function.identity()));

        if (config.enforceBean()) {
            List dataMappingErrors = collectDataMappingErrors(annotationMap.keySet(), dataJsonMap.keySet());
            if (!dataMappingErrors.isEmpty()) {
                throw new RuntimeException(
                        "Roq data is configured to enforce beans for data. Some data mapping and data files are not matching: %n%s"
                                .formatted(String.join(System.lineSeparator(), dataMappingErrors)));
            }
        }

        for (RoqDataBuildItem roqDataBuildItem : roqDataBuildItems) {
            String name = roqDataBuildItem.getName();
            if (annotationMap.containsKey(name)) {
                // Prepare mapping as typed bean
                AnnotationTarget target = annotationMap.get(name).target();
                if (!dataJsonMap.containsKey(name)) {
                    continue;
                }

                RoqDataBuildItem item = dataJsonMap.get(name);
                DotName className = target.asClass().name();
                // parent mapping
                final boolean isParentMapping = annotationMap.get(name)
                        .valueWithDefault(index.getIndex(), "parentArray").asBoolean();
                if (isParentMapping) {
                    final Optional parentMapping = target.asClass().constructors().stream()
                            .filter(this::isComplianceWithParentMapping)
                            .findAny();
                    final MethodInfo methodInfo = parentMapping.orElseThrow(() -> new RuntimeException(
                            "@DataMapping(parentArray=true) should declare a single parameter constructor with type List"));
                    final DotName type = methodInfo.parameterType(0).asParameterizedType().arguments().get(0).name();
                    dataMappingProducer.produce(new DataMappingBuildItem(
                            name,
                            className,
                            type, // need to get dynamically
                            item.getContent(),
                            item.converter(), target.asClass().isRecord()));
                    continue;
                }

                final DataMappingBuildItem roqMapping = new DataMappingBuildItem(
                        name,
                        null,
                        className,
                        item.getContent(),
                        item.converter(),
                        target.asClass().isRecord());

                dataMappingProducer.produce(roqMapping);
            } else {
                // Prepare mapping as JsonObject or JsonArray (we convert here to avoid one more step)
                try {
                    dataJsonProducer.produce(new RoqDataJsonBuildItem(name,
                            roqDataBuildItem.converter().convert(roqDataBuildItem.getContent())));
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }

    }

    private boolean isComplianceWithParentMapping(MethodInfo methodInfo) {
        if (methodInfo.parametersCount() == 1) {
            return methodInfo.parameterType(0).asParameterizedType().name()
                    .equals(ClassType.create(List.class).name());
        }
        return false;
    }

    private List collectDataMappingErrors(Set annotations, Set data) {
        List messages = new ArrayList<>();

        for (String name : annotations) {
            if (!data.contains(name)) {
                messages.add("The @DataMapping#value('%s') does not match with any data file".formatted(name));
            }
        }
        for (String name : data) {
            if (!annotations.contains(name)) {
                messages.add("The data file '%s' does not match with any @DataMapping class".formatted(name));
            }
        }
        return messages;
    }

    public Collection scanDataFiles(RoqProjectBuildItem roqProject,
            DataConverterFinder converter,
            BuildProducer watchedFilesProducer,
            RoqDataConfig config)
            throws IOException {

        Map items = new HashMap<>();

        final Consumer roqDirConsumer = (path) -> {
            if (Files.isDirectory(path)) {
                try (Stream pathStream = Files.find(path, Integer.MAX_VALUE,
                        (p, a) -> Files.isRegularFile(p) && isExtensionSupported(p))) {
                    pathStream.forEach(addRoqDataBuildItem(converter, watchedFilesProducer, path, items));
                } catch (IOException e) {
                    throw new RuntimeException("Error while scanning data files on location %s".formatted(path.toString()), e);
                }
            }
        };
        roqProject.consumePathFromRoqDir(config.dir(), roqDirConsumer);
        roqProject.consumePathFromRoqResourceDir(config.dir(), p -> roqDirConsumer.accept(p.getPath()));
        return items.values();
    }

    private static Consumer addRoqDataBuildItem(
            DataConverterFinder converter,
            BuildProducer watchedFilesProducer,
            Path rootDir,
            Map items) {
        return file -> {
            var name = rootDir.relativize(file).toString().replaceAll("\\..*", "").replaceAll("/", "_");
            if (items.containsKey(name)) {
                throw new RuntimeException("Multiple data files found for name: " + name);
            }
            String filename = file.getFileName().toString();
            if (Path.of("").getFileSystem().equals(file.getFileSystem())) {
                // We don't need to watch file out of the local filesystem
                watchedFilesProducer.produce(new HotDeploymentWatchedFileBuildItem(file.toAbsolutePath().toString(), true));
            }
            DataConverter dataConverter = converter.fromFileName(filename);

            if (dataConverter != null) {
                try {
                    items.put(name, new RoqDataBuildItem(name, Files.readAllBytes(file), dataConverter));
                } catch (IOException e) {
                    throw new UncheckedIOException("Error while decoding using %s converter: %s "
                            .formatted(filename, converter.getClass()), e);
                }
            }
        };
    }

    private static boolean isExtensionSupported(Path file) {
        String fileName = file.getFileName().toString();
        return SUPPORTED_EXTENSIONS.stream().anyMatch(fileName::endsWith);
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy