Skip to content

Commit 10b820a

Browse files
committed
#415 #393 clean up and removal of spring from Embedded Java app (generator to follow)
1 parent 443918e commit 10b820a

File tree

21 files changed

+456
-307
lines changed

21 files changed

+456
-307
lines changed

.idea/misc.xml

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
package com.thecoderscorner.embedcontrol.core.util;
2+
3+
import java.lang.reflect.Method;
4+
import java.lang.reflect.Parameter;
5+
import java.util.*;
6+
import java.util.concurrent.ConcurrentHashMap;
7+
import java.util.stream.Collectors;
8+
9+
/**
10+
* BaseMenuConfig is a base class for configuring menu settings and properties. It holds both the core configuration
11+
* and also extra configuration added by plugins, it is assumed that the plugin based configuration does not have any
12+
* dependencies outside of those that are declared in its parameters.
13+
*/
14+
public class BaseMenuConfig {
15+
protected final Map<Class<?>, Object> componentMap = new ConcurrentHashMap<>();
16+
protected final System.Logger logger = System.getLogger(getClass().getSimpleName());
17+
protected final String environment;
18+
protected final Properties resolvedProperties;
19+
protected final String baseName;
20+
21+
/**
22+
* this constructor ensures that the environment and properties are initialised. To configure the environment from
23+
* system properties set `env` to `null` and set system property `tc.env` to the environment name.
24+
* @param env the environment or null to evaluate from system properties.
25+
*/
26+
public BaseMenuConfig(String baseAppName, String env) {
27+
environment = (env != null) ? env : System.getProperty("tc.env", "dev");
28+
logger.log(System.Logger.Level.INFO, "Starting app in environment " + environment);
29+
baseName = baseAppName != null ? baseAppName : "application";
30+
resolvedProperties = resolveProperties(environment);
31+
}
32+
33+
/**
34+
* Resolves properties based on the specified environment.
35+
*
36+
* @param environment the environment for which properties need to be resolved
37+
* @return the resolved properties as a Properties object
38+
*/
39+
protected Properties resolveProperties(String environment) {
40+
Properties p = new Properties();
41+
try(var envProps = getClass().getResourceAsStream(String.format("/%s_%s.properties", baseName, environment));
42+
var globalProps = getClass().getResourceAsStream("/" + baseName + ".properties")) {
43+
if(globalProps != null) {
44+
logger.log(System.Logger.Level.INFO, "Reading global properties from " + globalProps);
45+
p.load(globalProps);
46+
}
47+
48+
if(envProps != null) {
49+
logger.log(System.Logger.Level.INFO, "Reading env properties from " + envProps);
50+
p.load(envProps);
51+
}
52+
logger.log(System.Logger.Level.INFO, "App Properties read finished");
53+
} catch (Exception ex) {
54+
logger.log(System.Logger.Level.ERROR, "Failed to read app property files", ex);
55+
}
56+
return p;
57+
}
58+
59+
/**
60+
* Scans for components annotated with {@code TcComponent} in the current class and stores them in the component map.
61+
* If an annotated method fails to invoke, an error message will be logged. Further, you can add parameters that will
62+
* be resolved, but this code cannot resolve circular dependencies, it will scan as long as at one component is built
63+
* each time around the loop.
64+
*/
65+
protected void scanForComponents() {
66+
var toResolve = Arrays.stream(getClass().getDeclaredMethods()).filter(m -> m.isAnnotationPresent(TcComponent.class))
67+
.collect(Collectors.toCollection(ArrayList::new));
68+
while(!toResolve.isEmpty()) {
69+
var resolvedThisTurn = new ArrayList<Method>();
70+
for (var m : toResolve) {
71+
m.setAccessible(true);
72+
try {
73+
if (m.getParameters().length == 0) {
74+
logger.log(System.Logger.Level.DEBUG, "Found " + m.getName() + " to fulfill " + m.getReturnType().getSimpleName());
75+
componentMap.put(m.getReturnType(), m.invoke(this));
76+
resolvedThisTurn.add(m);
77+
} else {
78+
var params = resolveParametersOrFail(m);
79+
if(params == null) continue;
80+
logger.log(System.Logger.Level.DEBUG, "Found " + m.getName() + " parameterized to fulfill " + m.getReturnType().getSimpleName());
81+
componentMap.put(m.getReturnType(), m.invoke(this, params.toArray()));
82+
resolvedThisTurn.add(m);
83+
}
84+
} catch (Exception e) {
85+
logger.log(System.Logger.Level.ERROR, "Skipping bean " + m.getName() + " because it failed", e);
86+
}
87+
}
88+
if(resolvedThisTurn.isEmpty() && !toResolve.isEmpty()) {
89+
throw new UnsupportedOperationException("Probably circular dependency in config, cannot resolve");
90+
} else {
91+
toResolve.removeAll(resolvedThisTurn);
92+
}
93+
}
94+
}
95+
96+
private ArrayList<Object> resolveParametersOrFail(Method m) {
97+
try {
98+
var params = new ArrayList<>();
99+
for (Parameter param : m.getParameters()) {
100+
Class<?> paramType = param.getType();
101+
Object resolvedParam = componentMap.get(paramType);
102+
if (resolvedParam == null) {
103+
resolvedParam = componentMap.entrySet().stream()
104+
.filter(e -> paramType.isAssignableFrom(e.getKey()))
105+
.findFirst()
106+
.orElseThrow()
107+
.getValue();
108+
}
109+
params.add(resolvedParam);
110+
}
111+
return params;
112+
} catch (Exception ex) {
113+
logger.log(System.Logger.Level.DEBUG, "Can't resolve " + m.getName() + " yet, unresolved dependencies");
114+
return null;
115+
}
116+
}
117+
118+
/**
119+
* Retrieves the current environment.
120+
*
121+
* @return the current environment as a String
122+
*/
123+
public String getEnvironment() {
124+
return environment;
125+
}
126+
127+
/**
128+
* Retrieves the resolved properties.
129+
*
130+
* @return the resolved properties as a Properties object
131+
*/
132+
public Properties getResolvedProperties() {
133+
return resolvedProperties;
134+
}
135+
136+
/**
137+
* Gets the value of the specified property as an integer. If the property exists in the resolved properties,
138+
* it will be parsed as an integer and returned. If the property does not exist, the default value will be returned.
139+
*
140+
* @param propName the name of the property to retrieve the value from
141+
* @param def the default value to return if the property does not exist
142+
* @return the value of the property as an integer, or the default value if the property does not exist
143+
*/
144+
protected int propAsIntWithDefault(String propName, int def) {
145+
if(resolvedProperties.containsKey(propName)) {
146+
return Integer.parseInt(resolvedProperties.getProperty(propName));
147+
}
148+
return def;
149+
}
150+
151+
/**
152+
* Retrieves the value of the specified property and throws an exception if missing.
153+
*
154+
* @param propName the name of the property to retrieve
155+
* @return the value of the property
156+
* @throws IllegalArgumentException if the property is missing in the configuration
157+
*/
158+
protected String mandatoryStringProp(String propName) {
159+
if(!resolvedProperties.containsKey(propName)) {
160+
throw new IllegalArgumentException("Missing property in configuration " + propName);
161+
}
162+
return resolvedProperties.getProperty(propName);
163+
}
164+
165+
/**
166+
* Retrieves an instance of the specified class from the component map. Fastest lookup is by direct class type.
167+
* However, if the class is not found in the map, it searches for a compatible class that would fulfill the
168+
* interface provided. If no compatible class is found, an exception is thrown.
169+
*
170+
* @param clazz the class to retrieve an instance of
171+
* @param <T> the generic type of the class, the function is T <= class T
172+
* @return an instance of the specified class
173+
* @throws NoSuchElementException if no compatible class is found in the component map
174+
*/
175+
public <T> T getBean(Class<T> clazz) {
176+
var cmp = componentMap.get(clazz);
177+
if(cmp == null) {
178+
cmp = componentMap.entrySet().stream().filter(e -> clazz.isAssignableFrom(e.getKey())).findFirst()
179+
.orElseThrow().getValue();
180+
}
181+
return (T) cmp;
182+
}
183+
184+
/**
185+
* Adds a bean to the component map and returns it.
186+
*
187+
* @param beanToAdd the bean to be added to the component map
188+
* @param <T> the type of the bean
189+
* @return the added bean
190+
*/
191+
public <T> T asBean(T beanToAdd) {
192+
componentMap.put(beanToAdd.getClass(), beanToAdd);
193+
return beanToAdd;
194+
}
195+
196+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.thecoderscorner.embedcontrol.core.util;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
/**
9+
* Methods within a MenuConfig annotated with this method will be called during construction and the returned result
10+
* will be added to componentMap so as to be available with its getBean method.
11+
*/
12+
@Retention(RetentionPolicy.RUNTIME)
13+
@Target(ElementType.METHOD)
14+
public @interface TcComponent {
15+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.thecoderscorner.embedcontrol.core.util;
2+
3+
import com.thecoderscorner.embedcontrol.core.controlmgr.color.ControlColor;
4+
import com.thecoderscorner.menu.domain.AnalogMenuItem;
5+
import com.thecoderscorner.menu.domain.AnalogMenuItemBuilder;
6+
import com.thecoderscorner.menu.domain.BooleanMenuItem;
7+
import com.thecoderscorner.menu.domain.BooleanMenuItemBuilder;
8+
import com.thecoderscorner.menu.persist.VersionInfo;
9+
import org.junit.jupiter.api.Test;
10+
11+
import static org.junit.jupiter.api.Assertions.assertEquals;
12+
13+
public class BaseMenuConfigTest {
14+
/**
15+
* Test case for `resolveProperties` method when global properties and environment-specific properties are both present.
16+
*/
17+
@Test
18+
public void testResolveProperties_GlobalAndEnvPropsPresent() throws Exception {
19+
TestMenuConfig config = new TestMenuConfig();
20+
assertEquals("bool name", config.getBean(BooleanMenuItem.class).getName());
21+
assertEquals(ControlColor.BLACK, config.getBean(ControlColor.class).getFg());
22+
assertEquals(ControlColor.WHITE, config.getBean(ControlColor.class).getBg());
23+
assertEquals("4.2.0", config.getBean(MenuAppVersion.class).getVersionInfo().toString());
24+
assertEquals("bool name", config.getBean(DependentItem.class).dependee().getName());
25+
assertEquals("12345", config.getBean(DependentItem.class).dependee2().getName());
26+
assertEquals("12345", config.getBean(AnalogMenuItem.class).getName());
27+
}
28+
29+
class TestMenuConfig extends BaseMenuConfig {
30+
public TestMenuConfig() {
31+
super("unittest_app", null);
32+
asBean(new ControlColor(ControlColor.BLACK, ControlColor.WHITE));
33+
asBean(new MenuAppVersion(VersionInfo.fromString("4.2.0"), "TS", "Group", "abc"));
34+
scanForComponents();
35+
}
36+
37+
@TcComponent
38+
DependentItem dependentItem(BooleanMenuItem dependee, AnalogMenuItem dependee2) {
39+
return new DependentItem(dependee, dependee2);
40+
}
41+
42+
@TcComponent
43+
AnalogMenuItem analogItem() {
44+
return new AnalogMenuItemBuilder().withName(mandatoryStringProp("dev.only.config")).withId(102)
45+
.menuItem();
46+
}
47+
48+
@TcComponent
49+
BooleanMenuItem booleanItem() {
50+
return BooleanMenuItemBuilder.aBooleanMenuItemBuilder().withId(100)
51+
.withName(mandatoryStringProp("bool.item.name")).menuItem();
52+
}
53+
}
54+
55+
public record DependentItem(BooleanMenuItem dependee, AnalogMenuItem dependee2) {
56+
}
57+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
bool.item.name=bool name
2+
dev.only.config=321
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dev.only.config=12345

embeddedJavaExample/src/main/java/com/thecoderscorner/menuexample/tcmenu/EmbeddedJavaDemoApp.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.thecoderscorner.menu.mgr.MenuInMenu;
44
import com.thecoderscorner.menu.mgr.MenuManagerServer;
5+
import com.thecoderscorner.menu.persist.MenuStateSerialiser;
56
import com.thecoderscorner.menu.remote.ConnectMode;
67
import com.thecoderscorner.menu.remote.LocalIdentifier;
78
import com.thecoderscorner.menu.remote.MenuCommandProtocol;
@@ -23,15 +24,15 @@ public class EmbeddedJavaDemoApp {
2324

2425
public EmbeddedJavaDemoApp() {
2526
context = new MenuConfig(null);
26-
manager = context.getMenuManagerServer();
27-
webServer = context.getWebServer();
27+
manager = context.getBean(MenuManagerServer.class);
28+
webServer = context.getBean(TcJettyWebServer.class);
2829
}
2930

3031
public void start() {
31-
var serializer = context.getMenuStateSerialiser();
32+
var serializer = context.getBean(MenuStateSerialiser.class);
3233
serializer.loadMenuStatesAndApply();
3334
Runtime.getRuntime().addShutdownHook(new Thread(serializer::saveMenuStates));
34-
manager.addMenuManagerListener(context.getMenuController());
35+
manager.addMenuManagerListener(context.getBean(EmbeddedJavaDemoController.class));
3536
buildMenuInMenuComponents();
3637
JfxLocalAutoUI.setAppContext(context);
3738
manager.addConnectionManager(webServer);
@@ -43,9 +44,9 @@ public static void main(String[] args) {
4344
}
4445

4546
public void buildMenuInMenuComponents() {
46-
MenuManagerServer menuManager = context.getMenuManagerServer();
47-
MenuCommandProtocol protocol = context.getWebServer().getProtocol();
48-
ScheduledExecutorService executor = context.getScheduledExecutorService();
47+
MenuManagerServer menuManager = context.getBean(MenuManagerServer.class);
48+
MenuCommandProtocol protocol = context.getBean(MenuCommandProtocol.class);
49+
ScheduledExecutorService executor = context.getBean(ScheduledExecutorService.class);
4950
LocalIdentifier localId = new LocalIdentifier(menuManager.getServerUuid(), menuManager.getServerName());
5051
var remMenuAvrBoardConnector = new SocketBasedConnector(localId, executor, Clock.systemUTC(), protocol, "192.168.0.96", 3333, ConnectMode.FULLY_AUTHENTICATED);
5152
var remMenuAvrBoard = new MenuInMenu(remMenuAvrBoardConnector, menuManager, menuManager.getManagedMenu().getMenuById(16).orElseThrow(), MenuInMenu.ReplicationMode.REPLICATE_ADD_STATUS_ITEM, 100000, 65000);

embeddedJavaExample/src/main/java/com/thecoderscorner/menuexample/tcmenu/EmbeddedJavaDemoController.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@
2424

2525
import static java.lang.System.Logger.Level.INFO;
2626

27+
/**
28+
* This class represents the controller for the EmbeddedJavaDemoMenu. It implements the MenuManagerListener interface.
29+
* The controller is responsible for creating instances of objects that are required around the application, you can
30+
* get hold of these objects later using getBean, and you can add extra ones wrapping an object creation with asBean(..).
31+
*
32+
* Unless you delete this file it will not be recreated as you can edit it too.
33+
* @see MenuManagerListener
34+
*/
2735
public class EmbeddedJavaDemoController implements MenuManagerListener {
2836
private final System.Logger logger = System.getLogger(getClass().getSimpleName());
2937
private final EmbeddedJavaDemoMenu menuDef;

0 commit comments

Comments
 (0)