AEM 6.3 - Migrate Felix to OSGi annotations: How to deal with propertyPrivate? - spring

I'm migrating an AEM 6.1 application to AEM 6.3. Since Felix annotations (org.apache.felix.scr.annotations.*) are deprecated, I decided to migrate my components to the OSGi annotations (org.osgi.service.component.annotations.*).
Once I figured out how it works, it is pretty easy. But there is one case I don't know how to handle: Properties with propertyPriavte = true.
The old implementation looks like this:
#Component(metatype = true)
#Service(Servlet.class)
#Properties({
#Property(name = "sling.servlet.selectors", value = "overlay", propertyPrivate = true),
})
public class OverlayServletImpl extends OverlayServlet {
...
}
The property sling.servlet.selectors would not be configurable in the Configuration Manager at the AEM console, but it would be configurable due to a config file, right? So, I still need to define this property.
For other properties I changed my implementation like this:
// OverlayServletImpl
#Component(
service = Servlet.class,
configurationPid = "my.package.path.OverlayServletImpl"
)
#Designate(
ocd = OverlayServletImplConfiguration.class
)
public class OverlayServletImpl extends OverlayServlet {
...
}
// Configuration
#ObjectClassDefinition(name = "Overlay Servlet")
public #interface OverlayServletImplConfiguration {
String sling_servlet_selectors() default "overlay";
...
}
Now, I have the property sling.servlet.selectors, but it is also available in Configuration Manager and it'S value can be changed there. But I don't want that.
How can I do that? Is this possible with the OSGi annotations?
Thank you and best regards!

It looks like this might be possible if you use the #Component annotation to specify your private properties.
#Component(service = Servlet.class,
property =
{ SLING_SERVLET_RESOURCE_TYPES + "=aemhtlexamples/structure/page",
SLING_SERVLET_METHODS + "=GET",
SLING_SERVLET_EXTENSIONS + "=html",
SLING_SERVLET_SELECTORS + "=hello" })
public class SimpleServlet extends SlingSafeMethodsServlet {
#Override
protected void doGet(final SlingHttpServletRequest req, final SlingHttpServletResponse resp)
throws ServletException, IOException {
final Resource resource = req.getResource();
resp.getOutputStream().println(resource.toString());
resp.getOutputStream().println("This content is generated by the SimpleServlet");
}
}
Source: https://github.com/heervisscher/htl-examples/blob/master/core/src/main/java/com/adobe/examples/htl/core/servlets/SimpleServlet.java

As far as I know this is not possible. Every property you define can be overridden by config.

Related

My application can't find the extension with Pf4j

I'm using a Spring Boot application. For now, the use of the plugins is very simple. I'm just following the tutorial. My plugin is started, I'm trying to find the extensions like this:
final List<MyExtensionPoint> sections = pluginManager.getExtensions(MyExtensionPoint.class);
but Pf4j doesn't return the extensions.
When I'm following the code execution, I can see this code in the AbstractExtensionFinder:
if (type.isAssignableFrom(extensionClass)) {
ExtensionWrapper extensionWrapper = createExtensionWrapper(extensionClass);
result.add(extensionWrapper);
log.debug("Added extension '{}' with ordinal {}", className, extensionWrapper.getOrdinal());
} else {
log.trace("'{}' is not an extension for extension point '{}'", className, type.getName());
if (RuntimeMode.DEVELOPMENT.equals(pluginManager.getRuntimeMode())) {
checkDifferentClassLoaders(type, extensionClass);
}
}
I can understand the program is not entering inside the condition because I have 2 different classloaders: PluginClassLoader (for the extension) and RestartClassLoader (from Spring for the interface of the extension point).
I don't understand why it will be a problem because I think to instanciate the extension class, the PluginClassloader will use the parent class loader (RestartClassLoader) to find the interface.
Where is my mistake ? How to fix it ?
Thank you.
I extended the DevelopmentPluginLoader to pass the Spring classloader.
public class MyDevelopmentPluginLoader extends DevelopmentPluginLoader {
private ClassLoader parentClassLoader;
/**
* #param pluginManager
*/
public MyDevelopmentPluginLoader(final PluginManager pluginManager, final ClassLoader parentClassLoader) {
super(pluginManager);
this.parentClassLoader = parentClassLoader;
}
#Override
protected PluginClassLoader createPluginClassLoader(final Path pluginPath, final PluginDescriptor pluginDescriptor) {
return new PluginClassLoader(pluginManager, pluginDescriptor, parentClassLoader);
}
}
and in my custom PluginManager, I created and instance of my PluginLoader:
protected PluginLoader createPluginLoader() {
final CompoundPluginLoader compoundPluginLoader = new CompoundPluginLoader();
final PluginLoader developmentPluginLoader = new MyDevelopmentPluginLoader(this, getClass().getClassLoader());
final PluginLoader jarPluginLoader = new JarPluginLoader(this);
final PluginLoader defaultPluginLoader = new DefaultPluginLoader(this);
// #formatter:off
return compoundPluginLoader.
add(developmentPluginLoader, this::isDevelopment).
add(jarPluginLoader, this::isNotDevelopment).
add(defaultPluginLoader, this::isNotDevelopment);
// #formatter:on
}

Read an app setting in my .Net 5 application without using DI

I created an MVC application using .Net 5.
Then I created a custom Attribute should read some settings from appsettings.json.
Here a working solution:
public class MyCustomAttribute : Attribute
{
public MyCustomAttribute(string key)
: base()
{
IConfiguration conf = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.Build();
var value = conf.GetValue<string>(key);
...
}
...
}
It works, but I do not think it is the correct solution. I try to explain why.
In the Program.cs, beforse the host is builded, the startup class is instantiated:
var builder = Host.CreateDefaultBuilder(args); builder
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
If I inject the IConfiguration to the stratup, I can get the appsettings.json values:
I think that from now, I have the appsettings.json and conseguently the configuration in memory.
So it seems really strange to me that in my custom attribute I must read the setting from the file again!
The question is: How can I read the in-memory configuration inside my custom attribute?
Is my consideration correct?
Thank you.
EDIT
I have found another solution, but I still does not like it:
I have modified the startup.cs ctor in this way:
public Startup(IWebHostEnvironment environment, IConfiguration configuration)
{
this._environment = environment;
this._configuration = configuration;
AppDomain.CurrentDomain.SetData("conf", this._configuration);
}
And the ctor of MyCustomAttribute in this way:
public class MyCustomAttribute : Attribute
{
public MyCustomAttribute(string key)
: base()
{
var conf = AppDomain.CurrentDomain.GetData("conf") as IConfiguration;
var value = conf.GetValue<string>(key);
...
}
...
}
Also this solution works. But I hope something out-of-the-box in .Net 5 exists. I would expect the normal behavior when the configuration is read, would be something similar to my solution.

JAXBElement: providing codec (/converter?) for class java.lang.Class

I have been evaluating to adopt spring-data-mongodb for a project. In summary, my aim is:
Using existing XML schema files to generate Java classes.
This is achieved using JAXB xjc
The root class is TSDProductDataType and is further modeled as below:
The thing to note here is that ExtensionType contains protected List<Object> any; allowing it to store Objects of any class. In my case, it is amongst the classes named TSDModule_Name_HereModuleType and can be browsed here
Use spring-data-mongodb as persistence store
This is achieved using a simple ProductDataRepository
#RepositoryRestResource(collectionResourceRel = "product", path = "product")
public interface ProductDataRepository extends MongoRepository<TSDProductDataType, String> {
TSDProductDataType queryByGtin(#Param("gtin") String gtin);
}
The unmarshalled TSDProductDataType, however, contains JAXBElement which spring-data-mongodb doesn't seem to handle by itself and throws a CodecConfigurationException org.bson.codecs.configuration.CodecConfigurationException: Can't find a codec for class java.lang.Class.
Here is the faulty statement:
TSDProductDataType tsdProductDataType = jaxbElement.getValue();
repository.save(tsdProductDataType);
I tried playing around with Converters for spring-data-mongodb as explained here, however, it seems I am missing something since the exception is about "Codecs" and not "Converters".
Any help is appreciated.
EDIT:
Adding converters for JAXBElement
Note: Works with version 1.5.6.RELEASE of org.springframework.boot::spring-boot-starter-parent. With version 2.0.0.M3, hell breaks loose
It seems that I missed something while trying to add converter earlier. So, I added it like below for testing:
#Component
#ReadingConverter
public class JAXBElementReadConverter implements Converter<DBObject, JAXBElement> {
//#Autowired
//MongoConverter converter;
#Override
public JAXBElement convert(DBObject dbObject) {
Class declaredType, scope;
QName name = qNameFromString((String)dbObject.get("name"));
Object rawValue = dbObject.get("value");
try {
declaredType = Class.forName((String)dbObject.get("declaredType"));
} catch (ClassNotFoundException e) {
if (rawValue.getClass().isArray()) declaredType = List.class;
else declaredType = LinkedHashMap.class;
}
try {
scope = Class.forName((String) dbObject.get("scope"));
} catch (ClassNotFoundException e) {
scope = JAXBElement.GlobalScope.class;
}
//Object value = rawValue instanceof DBObject ? converter.read(declaredType, (DBObject) rawValue) : rawValue;
Object value = "TODO";
return new JAXBElement(name, declaredType, scope, value);
}
QName qNameFromString(String s) {
String[] parts = s.split("[{}]");
if (parts.length > 2) return new QName(parts[1], parts[2], parts[0]);
if (parts.length == 1) return new QName(parts[0]);
return new QName("undef");
}
}
#Component
#WritingConverter
public class JAXBElementWriteConverter implements Converter<JAXBElement, DBObject> {
//#Autowired
//MongoConverter converter;
#Override
public DBObject convert(JAXBElement jaxbElement) {
DBObject dbObject = new BasicDBObject();
dbObject.put("name", qNameToString(jaxbElement.getName()));
dbObject.put("declaredType", jaxbElement.getDeclaredType().getName());
dbObject.put("scope", jaxbElement.getScope().getCanonicalName());
//dbObject.put("value", converter.convertToMongoType(jaxbElement.getValue()));
dbObject.put("value", "TODO");
dbObject.put("_class", JAXBElement.class.getName());
return dbObject;
}
public String qNameToString(QName name) {
if (name.getNamespaceURI() == XMLConstants.NULL_NS_URI) return name.getLocalPart();
return name.getPrefix() + '{' + name.getNamespaceURI() + '}' + name.getLocalPart();
}
}
#SpringBootApplication
public class TsdApplication {
public static void main(String[] args) {
SpringApplication.run(TsdApplication.class, args);
}
#Bean
public CustomConversions customConversions() {
return new CustomConversions(Arrays.asList(
new JAXBElementReadConverter(),
new JAXBElementWriteConverter()
));
}
}
So far so good. However, how do I instantiate MongoConverter converter;?
MongoConverter is an interface so I guess I need an instantiable class adhering to this interface. Any suggestions?
I understand the desire for convenience in being able to just map an existing domain object to the database layer with no boilerplate, but even if you weren't having the JAXB class structure issue, I would still be recommending away from using it verbatim. Unless this is a simple one-off project, you almost definitely will hit a point where your domain models will need to change but your persisted data need to remain in an existing state. If you are just straight persisting the data, you have no mechanism to convert between a newer domain schema and an older persisted data scheme. Versioning of the persisted data scheme would be wise too.
The link you posted for writing the customer converters is one way to achieve this and fits in nicely with the Spring ecosystem. That method should also solve the issue you are experiencing (about the underlying messy JAXB data structure not converting cleanly).
Are you unable to get that method working? Ensure you are loading them into the Spring context with #Component plus auto-class scanning or manually via some Configuration class.
EDIT to address your EDIT:
Add the following to each of your converters:
private final MongoConverter converter;
public JAXBElement____Converter(MongoConverter converter) {
this.converter = converter;
}
Try changing your bean definition to:
#Bean
public CustomConversions customConversions(#Lazy MongoConverter converter) {
return new CustomConversions(Arrays.asList(
new JAXBElementReadConverter(converter),
new JAXBElementWriteConverter(converter)
));
}

how to change the #FeignClient name in runtime

I use Spring Cloud Netflix to build my micro service .
#FeignClient(name = "ms-cloud",configuration = MsCloudClientConfig.class)
public interface TestClient {
/**
* #return
*/
#RequestMapping(value = "/test", method = RequestMethod.GET)
String test();
}
I want to change the name to ms-cloud-pre when some special user.
Anyone can give some advice?
According to the documentation feign supports placeholders in the name and url fields.
#FeignClient(name = "${store.name}")
public interface StoreClient {
//..
}
So you could set store.name=storeProd at runtime using normal spring boot configuration mechanisms.
To create a spring-cloud Feign client at runtime in situations where you don't know the service-id until the point of call:
import org.springframework.cloud.openfeign.FeignClientBuilder;
#Component
public class InfoFeignClient {
interface InfoCallSpec {
#RequestMapping(value = "/actuator/info", method = GET)
String info();
}
FeignClientBuilder feignClientBuilder;
public InfoFeignClient(#Autowired ApplicationContext appContext) {
this.feignClientBuilder = new FeignClientBuilder(appContext);
}
public String getInfo(String serviceId) {
InfoCallSpec spec =
this.feignClientBuilder.forType(InfoCallSpec.class, serviceId).build();
return spec.info();
}
}
That actually is possible. In Spring Cloud Zookeeper we're doing a similar thing since the name of the service in the Feign client is not the one that is there in the in Zookeeper. It can be an alias presented in the yaml file. Here you have the code example https://github.com/spring-cloud/spring-cloud-zookeeper/blob/master/spring-cloud-zookeeper-discovery/src/main/java/org/springframework/cloud/zookeeper/discovery/dependency/DependencyRibbonAutoConfiguration.java#L54 and here you have the description of the dependencies feature - https://github.com/spring-cloud/spring-cloud-zookeeper/blob/master/docs/src/main/asciidoc/spring-cloud-zookeeper.adoc#using-the-zookeeper-dependencies

#MessageMapping with placeholders

I am working with Spring-websocket and I have the following problem:
I am trying to put a placeholder inside a #MessageMapping annotation in order to get the url from properties. It works with #RequestMapping but not with #MessageMapping.
If I use this placeholder, the URL is null. Any idea or suggestion?
Example:
#RequestMapping(value= "${myProperty}")
#MessageMapping("${myProperty}")
Rossen Stoyanchev added placeholder support for #MessageMapping and #SubscribeMapping methods.
See Jira issue: https://jira.spring.io/browse/SPR-13271
Spring allows you to use property placeholders in #RequestMapping, but not in #MessageMapping. This is 'cause the MessageHandler. So, we need to override the default MessageHandler to do this.
WebSocketAnnotationMethodMessageHandler does not support placeholders and you need add this support yourself.
For simplicity I just created another WebSocketAnnotationMethodMessageHandler class in my project at the same package of the original, org.springframework.web.socket.messaging, and override getMappingForMethod method from SimpAnnotationMethodMessageHandler with same content, changing only how SimpMessageMappingInfo is contructed using this with this methods (private in WebSocketAnnotationMethodMessageHandler):
private SimpMessageMappingInfo createMessageMappingCondition(final MessageMapping annotation) {
return new SimpMessageMappingInfo(SimpMessageTypeMessageCondition.MESSAGE, new DestinationPatternsMessageCondition(
this.resolveAnnotationValues(annotation.value()), this.getPathMatcher()));
}
private SimpMessageMappingInfo createSubscribeCondition(final SubscribeMapping annotation) {
final SimpMessageTypeMessageCondition messageTypeMessageCondition = SimpMessageTypeMessageCondition.SUBSCRIBE;
return new SimpMessageMappingInfo(messageTypeMessageCondition, new DestinationPatternsMessageCondition(
this.resolveAnnotationValues(annotation.value()), this.getPathMatcher()));
}
These methods now will resolve value considering properties (calling resolveAnnotationValues method), so we need use something like this:
private String[] resolveAnnotationValues(final String[] destinationNames) {
final int length = destinationNames.length;
final String[] result = new String[length];
for (int i = 0; i < length; i++) {
result[i] = this.resolveAnnotationValue(destinationNames[i]);
}
return result;
}
private String resolveAnnotationValue(final String name) {
if (!(this.getApplicationContext() instanceof ConfigurableApplicationContext)) {
return name;
}
final ConfigurableApplicationContext applicationContext = (ConfigurableApplicationContext) this.getApplicationContext();
final ConfigurableBeanFactory configurableBeanFactory = applicationContext.getBeanFactory();
final String placeholdersResolved = configurableBeanFactory.resolveEmbeddedValue(name);
final BeanExpressionResolver exprResolver = configurableBeanFactory.getBeanExpressionResolver();
if (exprResolver == null) {
return name;
}
final Object result = exprResolver.evaluate(placeholdersResolved, new BeanExpressionContext(configurableBeanFactory, null));
return result != null ? result.toString() : name;
}
You still need to define a PropertySourcesPlaceholderConfigurer bean in your configuration.
If you are using XML based configuration, include something like this:
<context:property-placeholder location="classpath:/META-INF/spring/url-mapping-config.properties" />
If you are using Java based configuration, you can try in this way:
#Configuration
#PropertySources(value = #PropertySource("classpath:/META-INF/spring/url-mapping-config.properties"))
public class URLMappingConfig {
#Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
Obs.: in this case, url-mapping-config.properties file are in a gradle/maven project in src\main\resources\META-INF\spring folder and content look like this:
myPropertyWS=urlvaluews
This is my sample controller:
#Controller
public class WebSocketController {
#SendTo("/topic/test")
#MessageMapping("${myPropertyWS}")
public String test() throws Exception {
Thread.sleep(4000); // simulated delay
return "OK";
}
}
With default MessageHandler startup log will print something like this:
INFO: Mapped "{[/${myPropertyWS}],messageType=[MESSAGE]}" onto public java.lang.String com.brunocesar.controller.WebSocketController.test() throws java.lang.Exception
And with our MessageHandler now print this:
INFO: Mapped "{[/urlvaluews],messageType=[MESSAGE]}" onto public java.lang.String com.brunocesar.controller.WebSocketController.test() throws java.lang.Exception
See in this gist the full WebSocketAnnotationMethodMessageHandler implementation.
EDIT: this solution resolves the problem for versions before 4.2 GA. For more information, see this jira.
Update :
Now I understood what you mean, but I think that is not possible(yet).
Documentation does not mention anything related to Path mapping URIs.
Old answer
Use
#MessageMapping("/handler/{myProperty}")
instead of
#MessageMapping("/handler/${myProperty}")
And use it like this:
#MessageMapping("/myHandler/{username}")
public void handleTextMessage(#DestinationVariable String username,Message message) {
//do something
}
#MessageMapping("/chat/{roomId}")
public Message handleMessages(#DestinationVariable("roomId") String roomId, #Payload Message message, Traveler traveler) throws Exception {
System.out.println("Message received for room: " + roomId);
System.out.println("User: " + traveler.toString());
// store message in database
message.setAuthor(traveler);
message.setChatRoomId(Integer.parseInt(roomId));
int id = MessageRepository.getInstance().save(message);
message.setId(id);
return message;
}

Resources