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!
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:
postconstruct
runs, it should throw, if the appProperties
are
nullinit
, which again calls
initMenu
initMenu
appProperties
now is null, but it's final and
nothing but the c'tor should be able to change itEither one thing must be true:
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
annotationSo @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.