Search code examples
javaspring-bootcomponentsvaadinvaadin24

Why is ApplicationProperties null during @PostConstruct in my Vaadin Spring Boot application?


I am working on a Vaadin application using Spring Boot, and I am running into an issue where ApplicationProperties is null during the @PostConstruct method in my MenuLayout class. This causes the menu items to not be set up properly, and I see the following warning in my logs:

2024-09-03T13:09:08.059+03:00  WARN 6988 --- [io-8080-exec-10] c.r.application.layouts.MenuLayout       : ApplicationProperties is null. Menu items will not be set up.
2024-09-03T13:09:08.067+03:00  INFO 6988 --- [io-8080-exec-10] c.r.a.config.ApplicationProperties       : Unique Categories with Folders: {Transactions=[ObjectTransfers], Organizations=[Organization, Department], Metadata=[ResearcherComment, InfoText], Occurrences=[Event], Sources=[BookSource, OralHistorySource, ArchivalSources, WebSources, SourcePassageCollection, NewspaperPeriodical, Bibliography, SourcePassage], Entities=[Material, Persons, Objects, Collection, Route, DigitalObject, Objects]}

Here’s the relevant code for my MenuLayout class:

@Lazy
@Component
@DependsOn("applicationProperties")
public class MenuLayout extends HybridMenu implements RouterLayout {

    private static final long serialVersionUID = 1L;
    private static final Logger logger = Logger.getLogger(MenuLayout.class.getName());

    private final ApplicationProperties appProperties;

    @Autowired
    public MenuLayout(@Qualifier("applicationProperties") ApplicationProperties appProperties) {
        this.appProperties = appProperties;
    }

    @PostConstruct
    public void postConstruct() {
        // Check if ApplicationProperties is null
        if (this.appProperties == null) {
            logger.log(Level.SEVERE, "ApplicationProperties is null in postConstruct of MenuLayout.");
            throw new IllegalStateException("ApplicationProperties is null in postConstruct of MenuLayout.");
        }

        // Ensure VaadinSession is available
        VaadinSession vaadinSession = VaadinSession.getCurrent();
        if (vaadinSession == null) {
            throw new IllegalStateException("VaadinSession is not available.");
        }

        // Initialize the menu
        init(vaadinSession, UI.getCurrent());
    }

    @Override
    public boolean init(VaadinSession vaadinSession, UI ui) {
        initMenu();
        return true;
    }

    private void initMenu() {
        withConfig(new MenuConfig());
        getConfig().setTheme(ETheme.Lumo);

        if (appProperties != null) {
            setupMenuItems();
        } else {
            logger.log(Level.WARNING, "ApplicationProperties is null. Menu items will not be set up.");
        }
    }

    private void setupMenuItems() {
        // Menu setup code...
    }
}

this is the applicationProperties:

package com.ricontrans.application.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
@Primary
@Component
@ConfigurationProperties(prefix = "application")
public class ApplicationProperties {
    private final Map<String, Category> acceptedCategories = new HashMap<>();
    private final List<Description> descriptions = new ArrayList<>();
    private static final Logger logger = Logger.getLogger(ApplicationProperties.class.getName());

    public ApplicationProperties() {
        logger.log(Level.INFO, "ApplicationProperties constructor called.");
        loadProperties();
    }

    private void loadProperties() {
        logger.log(Level.INFO, "Loading properties from configuration.properties.");
        Properties properties = new Properties();
        try (InputStream input = getClass().getClassLoader().getResourceAsStream("configuration.properties")) {
            if (input == null) {
                logger.log(Level.WARNING, "configuration.properties not found.");
                return;
            }
            properties.load(input);

            // Load accepted categories (existing functionality)
            properties.forEach((key, value) -> {
                String keyString = (String) key;
                String valueString = (String) value;

                String[] keyParts = keyString.split("\\.");
                if (keyParts.length == 4 && "acceptedCategories".equals(keyParts[1])) {
                    String categoryName = keyParts[2];
                    String property = keyParts[3];

                    // Ensure that the category exists
                    Category category = acceptedCategories.computeIfAbsent(categoryName, k -> new Category());

                    // Update the category based on the property
                    switch (property) {
                        case "folder":
                            category.setFolder(valueString);
                            break;
                        case "schema":
                            category.setSchema(valueString);
                            break;
                        case "label":
                            category.setLabel(valueString);
                            break;
                        case "icon":
                            category.setIcon(valueString);
                            break;
                        case "tableProperties":
                            category.setTableProperties(valueString);
                            break;
                        case "Pie":
                            category.setPie(valueString);
                            break;
                        case "PieCategories":
                            category.setPieCategories(valueString);
                            break;
                        case "Map":
                            category.setMap(valueString);
                            break;
                        case "Time":
                            category.setTime(valueString);
                            break;
                        case "chartTypes":
                            category.setChartTypes(valueString);
                            break;
                        case "category":
                            category.setCategory(valueString);
                            break;
                        default:
                            break;
                    }
                }
            });

            logger.log(Level.INFO, "Accepted Categories: {0}", acceptedCategories);

            // Load descriptions for texts and images (new functionality)
            int index = 1;
            while (true) {
                String textPath = properties.getProperty("application.description." + index + ".text");
                String imagePath = properties.getProperty("application.description." + index + ".image");

                if (textPath == null && imagePath == null) {
                    break; // No more descriptions to load
                }

                Description description = new Description();
                if (textPath != null) {
                    description.setTextPath(textPath);
                    description.setLeft(Boolean.parseBoolean(properties.getProperty("application.description." + index + ".text.left", "false")));
                    description.setCenter(Boolean.parseBoolean(properties.getProperty("application.description." + index + ".text.center", "false")));
                    description.setRight(Boolean.parseBoolean(properties.getProperty("application.description." + index + ".text.right", "false")));
                }

                if (imagePath != null) {
                    description.setImagePath(imagePath);
                    description.setLeft(Boolean.parseBoolean(properties.getProperty("application.description." + index + ".image.left", "false")));
                    description.setCenter(Boolean.parseBoolean(properties.getProperty("application.description." + index + ".image.center", "false")));
                    description.setRight(Boolean.parseBoolean(properties.getProperty("application.description." + index + ".image.right", "false")));
                }

                description.setAlignWithLast(Boolean.parseBoolean(properties.getProperty("application.description." + index + ".alignWithLast", "false")));

                descriptions.add(description);
                index++;
            }

            logger.log(Level.INFO, "Descriptions loaded: {0}", descriptions);

        } catch (IOException ex) {
            logger.log(Level.SEVERE, "IOException occurred while loading properties", ex);
        }
    }

    public Map<String, Category> getAcceptedCategories() {
        return acceptedCategories;
    }

    public List<Description> getDescriptions() {
        return descriptions;
    }

    public String getCategoryFolder(String categoryName) {
        Category category = acceptedCategories.get(categoryName);
        return category != null ? category.getFolder() : null;
    }

    public Map<String, List<String>> getUniqueCategoriesWithFolders() {
        Map<String, List<String>> categoryFoldersMap = new HashMap<>();

        acceptedCategories.values().forEach(category -> {
            String categoryName = category.getCategory();
            String folder = category.getFolder();

            if (categoryName != null && folder != null) {
                // Get the list of folders for this category, or create a new one if it doesn't exist
                List<String> folders = categoryFoldersMap.computeIfAbsent(categoryName, k -> new ArrayList<>());
                // Add the folder to the list
                folders.add(folder);
            }
        });

        logger.log(Level.INFO, "Unique Categories with Folders: {0}", categoryFoldersMap);
        return categoryFoldersMap;
    }

    public static class Category {
        private String folder;
        private String schema;
        private String label;
        private String icon;
        private String tableProperties;
        private String Pie;
        private String PieCategories;
        private String Time;
        private String Map;
        private String chartTypes;
        private String category;
        private Properties properties = new Properties();

        // Getters and setters
        public String getFolder() {
            return folder;
        }

        public void setFolder(String folder) {
            this.folder = folder;
        }

        public String getSchema() {
            return schema;
        }

        public void setSchema(String schema) {
            this.schema = schema;
        }

        public String getLabel() {
            return label;
        }

        public void setLabel(String label) {
            this.label = label;
        }

        public String getIcon() {
            return icon;
        }

        public void setIcon(String icon) {
            this.icon = icon;
        }

        public String getTableProperties() {
            return tableProperties;
        }

        public void setTableProperties(String tableProperties) {
            this.tableProperties = tableProperties;
        }

        public String getPie() {
            return Pie;
        }

        public void setPie(String pie) {
            Pie = pie;
        }

        public String getPieCategories() {
            return PieCategories;
        }

        public void setPieCategories(String pieCategories) {
            PieCategories = pieCategories;
        }

        public String getTime() {
            return Time;
        }

        public void setTime(String time) {
            Time = time;
        }

        public String getMap() {
            return Map;
        }

        public void setMap(String map) {
            Map = map;
        }

        public String getChartTypes() {
            return chartTypes;
        }

        public void setChartTypes(String chartTypes) {
            this.chartTypes = chartTypes;
        }

        public String getCategory() {
            return category;
        }

        public void setCategory(String category) {
            this.category = category;
        }

        @Override
        public String toString() {
            return "Category{" +
                    "folder='" + folder + '\'' +
                    ", schema='" + schema + '\'' +
                    ", label='" + label + '\'' +
                    ", icon='" + icon + '\'' +
                    ", tableProperties='" + tableProperties + '\'' +
                    ", Pie='" + Pie + '\'' +
                    ", PieCategories='" + PieCategories + '\'' +
                    ", Time='" + Time + '\'' +
                    ", Map='" + Map + '\'' +
                    ", chartTypes='" + chartTypes + '\'' +
                    ", category='" + category + '\'' +
                    '}';
        }
    }

    public static class Description {
        private String textPath;
        private String imagePath;
        private boolean left;
        private boolean center;
        private boolean right;
        private boolean alignWithLast;
        private String image;
        private String text;

        // Getters and setters
        public String getTextPath() {
            return textPath;
        }

        public void setTextPath(String textPath) {
            this.textPath = textPath;
        }

        public String getImagePath() {
            return imagePath;
        }

        public void setImagePath(String imagePath) {
            this.imagePath = imagePath;
        }

        public boolean isLeft() {
            return left;
        }

        public void setLeft(boolean left) {
            this.left = left;
        }

        public boolean isCenter() {
            return center;
        }

        public void setCenter(boolean center) {
            this.center = center;
        }

        public boolean isRight() {
            return right;
        }

        public void setRight(boolean right) {
            this.right = right;
        }

        public boolean isAlignWithLast() {
            return alignWithLast;
        }

        public void setAlignWithLast(boolean alignWithLast) {
            this.alignWithLast = alignWithLast;
        }

        @Override
        public String toString() {
            return "Description{" +
                    "textPath='" + textPath + '\'' +
                    ", imagePath='" + imagePath + '\'' +
                    ", left=" + left +
                    ", center=" + center +
                    ", right=" + right +
                    ", alignWithLast=" + alignWithLast +
                    '}';
        }

        public String getImage() {
            return image;
        }

        public void setImage(String image) {
            this.image = image;
        }

        public String getText() {
            return text;
        }

        public void setText(String text) {
            this.text = text;
        }
    }
}

The main functionality I'm trying to achieve right here is I can set a dynamic layout hybrid menu, to all my views, and the info about the option of the menu is set in the configuration.properties file where the load of those properties are done into the ApplicationProperties.java! So I'm guessing the problem here is that for some reason menulayout is always initialized first from applicationProperties, because they are both components and maybe it has some necessary to be done first.

I tried a lot of things as you see:

  • Lazy Loading

  • Depend on the application properties

  • Post Construct.

    Also if I remove the post construct method, I have a problem about the VaadinSession that is null because the menulayout is marked as component and the session is not available at the moment!


Solution

  • TL;DR : Spring is used but not the Vaadin Spring integration; the layout is instantiated via the DefaultInstantiator and no DI is done. Either the configuration of the Vaadin Spring integration fails or did not activate or is not added at all.

    Also, RouterLayout must not be of singleton scope.


    Let's dissect this problem:

    • If postconstruct runs, it should throw, if the appProperties are null
    • but it happily seems to work and calls the init, which again calls initMenu
    • inside initMenu appProperties now is null, but it's final and nothing but the c'tor should be able to change it

    Either one thing must be true:

    • this is not the [MRE]
    • the log output does not represent the full timeline and in fact there are two different objects at play

    For the latter, there is an explanation:

    RouterLayout is indirectly managed by Spring, but directly via Vaadin -- visiting the site will create the views and their layouts for the view via the Instantiator facilities. If Spring is in play, instead of the DefaultInstantiator the SpringInstantiator will at last do the work.

    But there is no point/need to configure RouterLayout for Spring-DI and here it would just do harm.

    • @Lazy would delay instances to when they are needed; Vaadin takes care of this
    • @Component makes this a singleton and must be removed
    • @DependsOn("applicationProperties") is useless at best or another problem at worst, because the layout materializes due to the use inside a @Route annotation

    So @Component should make things worse by far (on reload, there would be errors, that components are already referenced from another UI). Since this seems not the problem, this strongly indicates the two different instances theory.

    The component is once handled by Spring (all the way, one would suspect, including passing the appProperties). And then Vaadin instances a new object, but not via Spring, but via the DefaultInstantiator. This does not pass in the appProperties and later fails.