In a project using Struts2 (2.3.20) I would like to run through the
configured actions (name, class, namespace, method) at application startup.
I'm using
Struts 2.3.20
struts-spring-plugin
struts-convention-plugin
For reference: I've done some work with beans and Struts injection before so not entirely fresh on this, but I'm stuck solving the problem stated here.
Any pointers on how to obtain this would be appreciated.
Further explanation
Reading Andrea's answer below I see I need to explain what I need.
I'm building a application menu builder feature for the application. My plan is to obtain the action configurations and build a tree of "menu nodes" from information in annotations on selected action classes and methods.
My problem with the code from the config-browser is that the Configuration (xwork) doesn't seem to be available outside of Struts components. Since this is an application startup task it doesn't really fit Struts' MVC component model. I'd like to put the menu building initialization in a ServletContextListener.
Fake example
Per request here is just the connection actionconfig <-> annotation <-> my_custom_menu. From this I could produce a menu structure provided from the annotations on action classes and methods.
public class ActionMenuBuilderListener implements ServletContextListener {
#Override
public void contextInitialized(ServletContextEvent arg0) {
List<ActionCfg> actions = Struts.getConfiguredActions(); // thisi is where I'd like some help
for(ActionCfg action : actions) {
MenuAnnotation annotation = getAnnotationFromMethodOrClass(action);
if(annotation != null) {
addMenuItem(action, annotation);
}
}
}
}
Here ActionCfgis whatever class Struts would return for action configuration, Struts.getConfiguredActions() would be one or more calls to Struts components and addMenu(...) is where I add a menu item node to my structure. The structure is later the target from JSP-s to build menus.
I don't know how much more code to write.
My sollution
For completness I thought I'll include what came out of this.
First, I to plugged in into Struts through this
ServletContextListnere:
public class ActionMenuBuilderListener implements
ServletContextListener {
#Override
public void contextDestroyed(ServletContextEvent arg0) {
}
#Override
public void contextInitialized(ServletContextEvent event) {
ActionMenuDispatcherListener listener =
new ActionMenuDispatcherListener();
ServletContext context = event.getServletContext();
listener.setServletContext(context);
Dispatcher.addDispatcherListener(listener);
}
}
Then, I wrote the DispatcherListener:
public class ActionMenuDispatcherListener implements DispatcherListener {
private ServletContext servletContext;
...
#Override
public void dispatcherInitialized(Dispatcher dispatcher) {
Map<String, PackageConfig> packages = dispatcher
.getConfigurationManager().getConfiguration()
.getPackageConfigs();
Map<String, Map<String, ActionConfig>> runtimeActionConfigs = dispatcher
.getConfigurationManager().getConfiguration()
.getRuntimeConfiguration().getActionConfigs();
for (String packageKey : runtimeActionConfigs.keySet()) {
Map<String, ActionConfig> actionConfigs = runtimeActionConfigs
.get(packageKey);
for (String actionKey : actionConfigs.keySet()) {
ActionConfig actionConfig = actionConfigs.get(actionKey);
PackageConfig packageConfig = packages.get(actionConfig
.getPackageName());
if (packageConfig != null) {
String actionName = actionConfig.getName();
String namespace = packageConfig.getNamespace();
try {
ActionMenu methodAnnotation = getMethodAnnotation(actionConfig);
if (methodAnnotation != null) {
String annotationInfo = methodAnnotation.value();
log.debug("[{}, {}, {}]", namespace, actionName,
annotationInfo);
}
} catch (ClassNotFoundException e) {
log.error("{}: {}", e.getClass().getSimpleName(),
e.getMessage());
}
}
}
}
}
protected ActionMenu getMethodAnnotation(ActionConfig actionConfig)
throws ClassNotFoundException {
String className = actionConfig.getClassName();
String methodName = actionConfig.getMethodName();
Class<?> actionClass = Class.forName(className);
try {
Method method = actionClass.getDeclaredMethod(methodName, null);
ActionMenu annotation = method.getAnnotation(ActionMenu.class);
return annotation;
} catch (NoSuchMethodException | SecurityException e) {
// log.error("{}: {}", e.getClass().getSimpleName(),
// e.getMessage());
}
return null;
}
}
Just in case someone else is thinking along those line :)
First of all you need to hook into application initialization process after the configurations are loaded and parsed. One of the ways is to implement DispatcherListener which you need to add to the Dispatcher. This you can do in ServletContextListener#contextInitialized method.
The second piece of the puzzle is to get action configurations. This is pretty simple because the instance of the Dispatcher is passed as argument into dispatcherInitialized method. To get all current action configurations get RuntimeConfiguration which holds data in Map<String, Map<String, ActionConfig>>, where the first map key is package namespace, the second map key is action name and ActionConfig holds all info about action. Since you need a class name then use getClassName() method of it.
public class ActionMenuBuilderListener implements ServletContextListener,DispatcherListener {
#Override
public void contextInitialized(ServletContextEvent sce) {
Dispatcher.addDispatcherListener(this);
}
#Override
public void dispatcherInitialized(Dispatcher du) {
Map<String, Map<String, ActionConfig>> runtimeActionConfigs = du
.getConfigurationManager().getConfiguration().getRuntimeConfiguration()
.getActionConfigs();
}
// other methods
}
And of course don't forget to register your listener in web.xml.
You are free of building this thing for your personal growth, but beware that it already exist.
It is called Config Browser Plugin (struts2-config-browser-plugin-2.3.20.jar).
It is included by default with the Maven archetypes, and you must remember of removing it before going in production.
Once imported it is available at the URL:
//www.SERVER_NAME.com:8080/WEBAPP_NAME/config-browser/actionNames
It gives you the exact informations you are looking for: actions, methods, results, parameters, mappings etc. and it looks like this:
Related
I'm trying to update an app from to Play 2.7. I see that now the access to the session object via Http.Context is deprecated. Instead I have to use the Http.Request object. Additionally before I could just change the Session object right away - now it seems like I have to create a new Session and add to the Result by myself. But how can I achieve this within an Action composition where I don't have access to the Result object?
An Action composition can look like:
public class VerboseAction extends play.mvc.Action.Simple {
public CompletionStage<Result> call(Http.Request req) {
...
return delegate.call(req);
}
}
I can't see how to add something to the Session here!
EDIT:
I couldn't find an easy solution but a workaround with a second action annotation. It's possible to access the Result object via .thenApply and attache the new Session object.
public CompletionStage<Result> call(Http.Request request) {
return delegate.call(request).thenApply(result -> {
Http.Session session = ... change the session
return result.withSession(session);
});
}
Still if someone has a better idea how to change the Session directly in the action composition please feel free to answer.
A session in cleared by withNewSession(). A new session is created when you add something with addingToSession(...), perhaps after a login. Here is my complete working code : I have 2 timestamp : one for the log file and one for an application timeout.
public class ActionCreator implements play.http.ActionCreator {
private final int msTimeout;
#Inject
public ActionCreator(Config config) {
this.msTimeout = config.getInt("application.msTimeout");
}
#Override
public Action<?> createAction(Http.Request request, Method actionMethod) {
return new Action.Simple() {
#Override
public CompletionStage<Result> call(Http.Request req) {
// add timestamp for the elapsed time in log
req.getHeaders().addHeader("x-log-timestamp", "" + System.currentTimeMillis());
// did a session timeout occur
boolean timeout = SessionUtils.isTimeout(req, msTimeout);
// apply current action
return delegate.call(req).thenApply(result -> {
// display some info in log
Utils.logInfo(req);
// return final result
if (timeout) {
return result.withNewSession();
} else if (SessionUtils.isOpen(req)) {
return result.addingToSession(req, "timestamp", "" + System.currentTimeMillis());
} else {
return result;
}
});
}
};
}
}
We are using Guice in our project for DI. Currently we have some configurations(properties) that we load a t server startup from a file. These are then bound to all the components & used for all the requests.
But now, we have multiple property files & load them at startup. These configurations can be different per REST(Jersey) request as they depend on the input.
So, we need to bind these configurations dynamically for each request. I looked into Guice API for #RequestScoped, but did not find anything specificallyu helpful.
There are few questions similar to this, but no luck yet. Can you please help me with this.
I'm providing 2 ways of doing this and both are request scoped.
Using HttpServletRequest, for classes where you can Inject request object.
Using ThreadLocal, Generic way. It can be used in any class.
(NOTE: This method wouldn't work if your creating new threads in your code and want to access the value. In which case you'll have to pass the values through Objects to those threads)
I meant something like this:
public class RequestFilter implements ContainerRequestFilter {
#Context
private HttpServletRequest request;
#Override
public void filter(ContainerRequestContext requestContext) throws IOException {
List listOfConfig = //load Config;
request.setAttribute("LOADED_CONFIG",listOfConfig);
// If you want to access this value at some place where Request object cannot be injected (like in service layers, etc.) Then use below ThreadLocals.
ThreadLocalWrapper.getInstance().get().add("adbc"); // In general add your config here, instead of abdc.
}
}
My ThreadLocalWrapper looks like this:
public class ThreadLocalWrapper {
private static ThreadLocal<List<String>> listOfStringLocals; // You can modify this to a list of Object or an Object by itself.
public static synchronized ThreadLocal<List<String>> getInstance() {
if (listOfStringLocals == null) {
listOfStringLocals = new ThreadLocal<List<String>>() {
#Override
protected List<String> initialValue() {
return new ArrayList<String>();
}
};
}
return listOfStringLocals;
}
}
To Access the value:
In Controller - Inject HttpServletRequest Object and do getAttribute() to get the value. Since HttpServletRequest Object is requestScoped, you can set the loaded config. into this and access it in your controller's using request Object again.
In Any other part of the code - If HttpServletRequest is not available then you can always use the ThreadLocal example shown. To access this value.
public class GuiceTransactionImpl implements GuiceTransaction {
private String value = "";
public GuiceTransactionImpl(String text) {
value = text;
}
#Override
public String returnSuccess() {
return value + " Thread Local Value " + ThreadLocalWrapper.getInstance().get();
}
}
I will start by saying that:
I'm using ODA (godmode,khan,marcel).
I'm the only code signer.
sessionAsSigner is working the first time I load an XPage that calls it.
sessionAsSigner becomes null after I reload a page (cmd + R) but not when I'm subsequently referencing it in any action during in the context of the viewScope lifetime.
I'm implementing #Gidgerby 's concept of controller classes
I would also add that sessionAsSigner works consistently if I just prepare a simple XPage that does the following:
<p>
<xp:text value="session: #{session.effectiveUserName}" />
</p>
<p>
<xp:text value="sessionAsSigner: #{sessionAsSigner.effectiveUserName}" />
</p>
Now, I am not using SSJS. I'm JSF/EL oriented as much as I can, according to my current knowledge. So, the way I access Domino back-end is unconventional for a Domino XPages programmer.
Where I can't get getSessionAsSigner to work consistently is when I try to do the above mentioned thing...
Here is my XPage controller (backing bean):
public class TestPageController extends StandardXPageController {
private static final long serialVersionUID = 1L;
private AnswerDTO answer;
public TestPageController() {
loadQuotation();
}
private void loadQuotation() {
AnswerDAO answerDAO = Factory.createAnswerDAO();
try {
answer = answerDAO.read("doc_id");
} catch (Exception e) {
e.printStackTrace();
}
}
public AnswerDTO getAnswer() {
return answer;
}
}
AnswerDTO is a POJO. I'm using the DAO/DTO design pattern.
The AnswerDAO implementation - with simplified code (wrap method is just a mere mapping of fields) - as following:
public class AnswerDAODominoImpl implements AnswerDAO {
private transient Session s;
private transient Database db;
private Session getSession() {
if (s == null) {
s = XSPUtil.getCurrentSessionAsSigner();
}
return s;
}
private Database getDatabase() throws NotesException {
if (db == null) {
db = getSession().getDatabase("server_name", "server_path");
}
return db;
}
public AnswerDTO read(String id) throws Exception {
Database db = getDatabase();
return wrap(db.getDocumentByID(id));
}
}
This is the ViewHandler class:
public class ViewHandler extends ViewHandlerExImpl {
public ViewHandler(final javax.faces.application.ViewHandler delegate) {
super(delegate);
}
#SuppressWarnings("unchecked")
public UIViewRoot createView(final FacesContext context, final String pageName) {
try {
// pageName is the XPage name prefixing the slash (e.g. "/home")
String pageClassName = pageName.substring(1);
Class<? extends XPageController> controllerClass = null;
try {
controllerClass = (Class<? extends XPageController>) context.getContextClassLoader().loadClass(
"com.sea.xsp.controller." + StringUtil.getProperCaseString(pageClassName) + "PageController");
} catch (ClassNotFoundException cnfe) {
controllerClass = StandardXPageController.class;
}
XPageController controller = controllerClass.newInstance();
Map<String, Object> requestScope = (Map<String, Object>) context.getApplication().getVariableResolver().resolveVariable(context, "requestScope");
requestScope.put("controller", controller);
UIViewRootEx root = (UIViewRootEx) super.createView(context, pageName);
root.getViewMap().put("controller", controller);
requestScope.remove("controller");
// MethodBinding beforePageLoad = context.getApplication().createMethodBinding("#{controller.beforePageLoad}", new Class[] { PhaseEvent.class });
// root.setBeforePageLoad(beforePageLoad);
return root;
} catch (Exception e) {
e.printStackTrace();
}
return super.createView(context, pageName);
}
}
Basically, what the viewhandler does is to check the existence of a java class which prefix is the XPage name itself.
eg. test.xsp = something.something.package.TestPageController
This approach allows me to forget about declaring specific XPage related classes as generic managed beans in the faces-config.xml
All the XPages will get an easy handle to their corresponding backing bean that will always be named #{controller}
Now, having that being said if I simply write the following in an XPage, everything will work the first time, but not a second time (getSession() is OK, getSessionAsSigner is null), never ever again. I need to push a new change to the database (design update after any change to the java code and xsp.application.forcefullrefresh=true) and it will work again, but, again, just the first time the page is loaded.
<p>
<xp:text value="answer doc id: #{controller.answer.id}" />
</p>
Ideas?
This is possibly due to a bug we discovered a bit ago with the XPages runtime, somehow related to ClassLoader#loadClass. It turns out that using that as you do (and as I used to) can cause sessionAsSigner to stop working after the first page load. The fix is to switch to Class.forName(String, true, ClassLoader): https://github.com/jesse-gallagher/XPages-Scaffolding/commit/d65320fd6d98ff2fbaa814a95eb38ce7bad5a81d
What happens if you run through in Java debugger? Is it going into XSPUtil.getSessionAsSigner() the second time round?
I tend to use ExtLibUtil.resolveVariable() to get a handle on sessionAsSigner if godmode is enabled. Alternatively, there's org.openntf.domino.utils.Factory.getSessionAsSigner(). Do those give a different result?
(In RC3 that is still be available but is marked deprecated in favour of Factory.getSession(SessionType.SIGNER) because we're supporting implementations outside XPages, so there are more session types involved.)
I'm using slf4j + log4j in java to add logging to my web application. In each logged event I want to print the feature the user is executing and an identifier of the user action.
When my webapp catches the user's request, I generate the identifier and I have access to the feature "name". What is the best way to put these two parameters in every event logged without passing it out to every method log.(info|warn|debug|error)?
I know I can use MDC context to make the parameters visible for the whole thread and print them in the formatter of the logging statement.
log4j.appender.AppLog.layout.ConversionPattern = %d{ddMMyyyyHHmmss}|%X{feature}|%-5p|%t|%l|%X{actionId}|%m%n
But I wanted a more controlled solution. Note that in the example above the feature and actionId aren't printed throught the logging message, but this isn't a requirement.
I would do something like this:
public MyClass {
Logger log = new InterceptedLogger("com.foo.MyClass");
// ...
public void handleUserAction() {
log.info("Action detected !");
// ...
}
}
public InterceptedLogger extends Logger {
private static MyFramework myFrameWork = MyFramework.getInstance(); // or use an IoC to inject the instance for you...
public InterceptedLogger(String name) {
super(name);
}
public void info(Object message) {
if (isInfoEnable()) {
super.info( //
String.format( //
"%s#%s - %s", // this format may be configured externally
myFrameWork.getCurrentUser(), //
myFrameWork.getCurrentAction(), //
message //
));
}
}
// Override other methods as needed
}
My problem concerns the creation of a custom method within an action. I'm using Struts2 and REST Plugin in order to implement a RESTful WebService. My action class is the following:
public class SampleController implements ModelDriven<Object> {
private Sample sample = new Sample();
private Collection<Sample> list;
private int id;
public HttpHeaders create() {
sdao.save(sample);
return new DefaultHttpHeaders("create");
}
public HttpHeaders destroy() {
return new DefaultHttpHeaders("destroy");
}
public HttpHeaders show() {
return new DefaultHttpHeaders("show").disableCaching();
}
public HttpHeaders update() {
sdao.save(sample);
return new DefaultHttpHeaders("update");
}
public HttpHeaders index() {
list = sdao.findAll();
return new DefaultHttpHeaders("index").disableCaching();
}
public Object getModel() {
return (list != null ? list : sample);
}
public int getId() {
return id;
}
public void setId(Integer id) {
if (id != null) {
this.sample = (Sample) sdao.findById(id);
}
this.id = id;
}
}
I can access to a resource via a GET HTTP method correctly. In order to use a custom method, called by passing a parameter to search resources i.e
public searchBySenderName(String senderName) {
list.addAll(sdao.findBySenderName(senderName))
}
What is the correct procedures? How can I call it via GET following URL?
You can call custom method from any of the predefined methods for GET (index, show) in your case, see RESTful URL mapping logic .
RESTful URL Mapping Logic
This Restful action mapper enforces Ruby-On-Rails REST-style mappings.
If the method is not specified (via '!' or 'method:' prefix), the
method is "guessed" at using REST-style conventions that examine the
URL and the HTTP method. Special care has been given to ensure this
mapper works correctly with the codebehind plugin so that XML
configuration is unnecessary.
Of course you can change the method names used by the action mapper, but it will affect a whole application. If you already occupied a resource URL then you should use another to perform its job. This is in case if you are using a strict rest mapper. In the mixed mode you can map an usual action to some action method.
REST and non-RESTful URL's Together Configuration
If you want to keep using some non-RESTful URL's alongside your REST
stuff, then you'll have to provide for a configuration that utilizes
to mappers.
Plugins contain their own configuration. If you look in the Rest
plugin jar, you'll see the struts-plugin.xml and in that you'll see
some configuration settings made by the plugin. Often, the plugin just
sets things the way it wants them. You may frequently need to override
those settings in your own struts.xml.
And last, you mightn't specify a method via ! or method: prefix because it's restricted by default configuration.