Struts2 annotations (Convention Plugin) - java

I meet an odd problem with struts2 annotation, let me elaborate it first
#Results({
#Result(name = "input", location = "main.jsp"),
#Result(name = "list", location = "list.jsp")
})
public class MainAction extends ActionSupport {
private PortalUser user;
#Autowired
private PortalUserService portalUserService;
public String execute() throws Exception {
return INPUT;
}
#Action("addUser")
public String addUser() throws Exception {
portalUserService.addUser(user);
return listUser();
}
#Action("listUser")
#SkipValidation
public String listUser() throws Exception {
List theUserList = portalUserService.getPortalUserList(null);
ServletActionContext.getRequest().setAttribute("userList", theUserList);
return "list";
}
#Action("modifyUser")
public String modifyUser() throws Exception {
List theUserList = portalUserService.getPortalUserList(null);
ServletActionContext.getRequest().setAttribute("userList", theUserList);
return "list";
}
public void validate() {
if (user != null && StringUtils.isBlank(user.getUserName()))
addFieldError("accountBean.userName", "User name is required.");
System.out.println("validate #####");
}
public PortalUser getUser() {
return user;
}
public void setUser(PortalUser user) {
this.user = user;
}
}
this is the struts2 action class, I configure it correctly and type the url
http://domain/listUser it will list all users
http://domain/modifyUser it can modify the users
all things go well in tomcat with exploded class files
But when I build with the war file and deploy it into tomcat webapp folder, the page report
there is no action name listUser.
The difference between the two scenario is exploded class files and archived class files that I compile and jar the action and other class files into it.
I was puzzled about this phenomenon.
So any suggestions and advices will be very appreciated!

I experiment as followings and conclude it that with struts2 annotation,the action class can't move into jar files,it must be located in your WEB-INF/classes
I acknowledge that the struts2 convention will scan action class in the specific package,so I left only the action classes in WEB-INF/classes/.../action folder ,jar other class files and put it into WEB-INF/lib, it's done

By default the plugin does not scan jars or the classpath, solely WEB-INF/classes. You might want to see the plugin's Configuration Reference and look for the value struts.convention.action.includeJars, which lets you list the jars where you also want to look for the files.

Related

Condition fails in Junit5 & Mockito

I am trying to write the test case for my application and I cannot get past a condition even after providing what is expected, from what I know.
Here is my test class.
#ExtendWith(MockitoExtension.class)
class AppConfigTest {
#Mock
#TempDir
File mockedFile;
#InjectMocks
private AppConfig appConfig;
#Test
void getData() throws Exception {
File f = new File("f");
File[] files = {f};
lenient().when(mockedFile.listFiles()).thenReturn(files);
lenient().when(mockedFile.isFile()).thenReturn(true);
assertNotNull(appConfig.getData());
}
}
My implementation. The test doesn't go past the if condition. The test does not cover the code after the condition as it turns true all the time. I need my test to cover keyMap() in the last line.
private Map<String, String> getData() {
File[] files = new File(APP_CONST.DIRECTORY).listFiles();
if (null == files) { // not turning FALSE even after providing mocked "files" array
return Collections.emptyMap();
}
List<String> keysList = getKeyList(files);
return keyMap(APP_CONST.DIRECTORY, keysList);
}
Can anyone please tell me how to correct this please? Using SpringBoot/JUnit 5
We discussed this in the comments, but in any case, I guess an example is better.
One way you could go about this is to make sure the same folder exists. In the test setup you could simply create it.
#Before
public void setUp() {
new File(APP_CONST.DIRECTORY).mkdirs();
}
Now when accessing it in the implementation there will be a directory. You can also inside the test add files to the directory, so it's not empty.
Although this works, it has some issues with setting it up and cleaning it up. A better way is to abstract this from the implementation itself and use some kind of provider for it.
A suggestion would be to create an interface where the real implementation returns the real folder and in tests you can mock this.
public interface DirectoryProvider {
public File someDirectory();
}
public class RealDirectoryProvider implements DirectoryProvider {
#Override
public File someDirectory() {
return new File(APP_CONST.DIRECTORY);
}
}
you can now make the getData class depend on this abstraction. You didn't give us the class name, so don't pay attention to that part:
public class Data {
private final DirectoryProvider directoryProvider;
public Data(DirectoryProvider directoryProvider) {
this.directoryProvider = directoryProvider;
}
private Map<String, String> getData() {
File[] files = directoryProvider.someDirectory().listFiles();
if (null == files) {
return Collections.emptyMap();
}
List<String> keysList = getKeyList(files);
return keyMap(APP_CONST.DIRECTORY, keysList);
}
}
Now during the test you can just inject your mocked directory/temp dir.
#ExtendWith(MockitoExtension.class)
class AppConfigTest {
#TempDir
File mockedFile;
#Mock
DirectoryProvider directoryProvider;
#InjectMocks
private AppConfig appConfig;
#Test
void getData() throws Exception {
lenient().when(directoryProvider.someDirectory()).thenReturn(mockedFile);
assertNotNull(appConfig.getData());
}
}
You can also add files to the temp dir if you need. This however should be enough to pass the if I think.

Unable to resolve variable from properties file when tried to access as function parameter using #Value annotation

This may be silly question to ask but i'm unable to find any satisfactory solution to my problem. In java we don't have the concept of default variables so i am trying to give default value from properties file to my function parameters/arguments using #Value annotation, but i'm always getting null and i'm unable to figure why is this happening. Please help me to solve the issue or provide me some appropriate link/reference which may solve my issue.
MainApplication.java
#SpringBootApplication
public class Application
{
public static void main(String[] args)
{
ApplicationContext context = SpringApplication.run(NetappApplication.class, args);
Sample sample = context.getBean(Sample.class);
System.out.println(sample.check(null));
}
}
Sample.java
public interface Sample
{
public String check(String message);
}
SampleImpl.java
#Service
#PropertySource("classpath:app.properties")
public class SampleImpl implements Sample
{
#Value("${test}")
String message1;
#Override
public String check(#Value("${test}") String message)
{
return message;
}
}
app.properties
test=anand
But you are passing null to your method...
Perhaps what you want to do is to assign default value to test in case it's not defined in property file:
#Value("${test:default}");
Then, when properties are autowired by Spring if placeholder resolver doesn't get the value from props file, it will use what is after :.
The best use case for this (that I can think of) is when you create Spring configuration.
Let's say you have a configuration class: for DB access. Simply put:
#Configuration
public class DbConfig {
#Value("${url:localhost}")
String dbUrl;
// rest for driver, user, pass etc
public DataSource createDatasource() {
// here you use some DataSourceBuilder to configure connection
}
}
Now, when Spring application starts up, properties' values are resolved, and as I wrote above you can switch between value from property and a default value. But it is done once, when app starts and Spring creates your beans.
If you want to check incoming argument on runtime, simple null check will be enough.
#Value("${test}")
String message1;
#Override
public String check(String message) {
if (message == null) {
return message1;
}
}

Dynamic applicationpath

A new application of ours uses multi-tenancy with multiple database. By providing a tenant id in the URL, we can select the right datasource.
But by using that kind of method, the namespace of the URL becomes dynamic (e.g.: instead of /api the url changes to /{id}/api). So is it possible to use a dynamic #ApplicationPath?
Just as it is possible to use a variable in the #Path annotation, could I write something like #ApplicationPath("/tenants/{id}/api")?
Seems applicationpath does not support dynamic segments. In the end we fixed it by using sub-resources:
Config
#ApplicationPath("tenants")
public class TenantConfig extends ResourceConfig {
public TenantConfig(ObjectMapper mapper) {
//set provider + add mapper
register(TenantsController.class);
}
}
TenantsController
#Path("/{id}/api")
public class TenantsController {
//register all your controllers including path here
#Path("/somethings")
public Class<SomethingController> something() {
return SomethingController.class;
}
}
SomethingController
#Component
//Don't use #Path, as path info is already defined in the TenantsController
public class SomethingController {
//do your stuff here;
#GET
#Path("/{id}") //Path for this example would be /tenants/{id}/api/somethings/{id}
public JsonApiResult get(#PathParam("id") int id) {
//retrieve one something
}
}

Converting & validating CSV file upload in Spring MVC

I have a Customer entity that contains a list of Sites, as follows:
public class Customer {
#Id
#GeneratedValue
private int id;
#NotNull
private String name;
#NotNull
#AccountNumber
private String accountNumber;
#Valid
#OneToMany(mappedBy="customer")
private List<Site> sites
}
public class Site {
#Id
#GeneratedValue
private int id;
#NotNull
private String addressLine1;
private String addressLine2;
#NotNull
private String town;
#PostCode
private String postCode;
#ManyToOne
#JoinColumn(name="customer_id")
private Customer customer;
}
I am in the process of creating a form to allow users to create a new Customer by entering the name & account number and supplying a CSV file of sites (in the format "addressLine1", "addressLine2", "town", "postCode"). The user's input needs to be validated and errors returned to them (e.g. "file is not CSV file", "problem on line 7").
I started off by creating a Converter to receive a MultipartFile and convert it into a list of Site:
public class CSVToSiteConverter implements Converter<MultipartFile, List<Site>> {
public List<Site> convert(MultipartFile csvFile) {
List<Site> results = new List<Site>();
/* open MultipartFile and loop through line-by-line, adding into List<Site> */
return results;
}
}
This worked but there is no validation (i.e. if the user uploads a binary file or one of the CSV rows doesn't contain a town), there doesn't seem to be a way to pass the error back (and the converter doesn't seem to be the right place to perform validation).
I then created a form-backing object to receive the MultipartFile and Customer, and put validation on the MultipartFile:
public class CustomerForm {
#Valid
private Customer customer;
#SiteCSVFile
private MultipartFile csvFile;
}
#Documented
#Constraint(validatedBy = SiteCSVFileValidator.class)
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
public #interface SiteCSVFile {
String message() default "{SiteCSVFile}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class SiteCSVFileValidator implements ConstraintValidator<SiteCSVFile, MultipartFile> {
#Override
public void initialize(SiteCSVFile siteCSVFile) { }
#Override
public boolean isValid(MultipartFile csvFile, ConstraintValidatorContext cxt) {
boolean wasValid = true;
/* test csvFile for mimetype, open and loop through line-by-line, validating number of columns etc. */
return wasValid;
}
}
This also worked but then I have to re-open the CSV file and loop through it to actually populate the List within Customer, which doesn't seem that elegant:
#RequestMapping(value="/new", method = RequestMethod.POST)
public String newCustomer(#Valid #ModelAttribute("customerForm") CustomerForm customerForm, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "NewCustomer";
} else {
/*
validation has passed, so now we must:
1) open customerForm.csvFile
2) loop through it to populate customerForm.customer.sites
*/
customerService.insert(customerForm.customer);
return "CustomerList";
}
}
My MVC config limits file uploads to 1MB:
#Bean
public MultipartResolver multipartResolver() {
CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
multipartResolver.setMaxUploadSize(1000000);
return multipartResolver;
}
Is there a spring-way of converting AND validating at the same time, without having to open the CSV file and loop through it twice, once to validate and another to actually read/populate the data?
IMHO, it is a bad idea to load the whole CSV in memory unless :
you are sure it will always be very small (and what if a user click on wrong file ?)
the validation is global (only real use case, but does not seem to be here)
your application will never be used in a production context under serious load
You should either stick to the MultipartFile object, or use a wrapper exposing the InputStream (and eventually other informations you could need) if you do not want to tie your business classes to Spring.
Then you carefully design, code and test a method taking an InputStream as input, reads it line by line and call line by line methods to validate and insert data. Something like
class CsvLoader {
#Autowired Verifier verifier;
#Autowired Loader loader;
void verifAndLoad(InputStream csv) {
// loop through csv
if (verifier.verify(myObj)) {
loader.load(myObj);
}
else {
// log the problem eventually store the line for further analysis
}
csv.close();
}
}
That way, your application only uses the memory it really needs, only looping once other the file.
Edit : precisions on what I meant by wrapping Spring MultipartFile
First, I would split validation in 2. Formal validation is in controller layer and only controls that :
there is a Customer field
the file size and mimetype seems Ok (eg : size > 12 && mimetype = text/csv)
The validation of the content is IMHO a business layer validation and can happen later. In this pattern, SiteCSVFileValidator would only test csv for mimetype and size.
Normally, you avoid directly using Spring classes from business classes. If it is not a concern, the controller directly sends the MultipartFile to a service object, passing also the BindingResult to populate directly the eventual error messages. The controller becomes :
#RequestMapping(value="/new", method = RequestMethod.POST)
public String newCustomer(#Valid #ModelAttribute("customerForm") CustomerForm customerForm, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "NewCustomer"; // only external validation
} else {
/*
validation has passed, so now we must:
1) open customerForm.csvFile
2) loop through it to validate each line and populate customerForm.customer.sites
*/
customerService.insert(customerForm.customer, customerForm.csvFile, bindingResult);
if (bindingResult.hasErrors()) {
return "NewCustomer"; // only external validation
} else {
return "CustomerList";
}
}
}
In service class we have
insert(Customer customer, MultipartFile csvFile, Errors errors) {
// loop through csvFile.getInputStream populating customer.sites and eventually adding Errors to errors
if (! errors.hasErrors) {
// actually insert through DAO
}
}
But we get 2 Spring classes in a method of service layer. If it is a concern, just replace the line customerService.insert(customerForm.customer, customerForm.csvFile, bindingResult); with :
List<Integer> linesInError = new ArrayList<Integer>();
customerService.insert(customerForm.customer, customerForm.csvFile.getInputStream(), linesInError);
if (! linesInError.isEmpty()) {
// populates bindingResult with convenient error messages
}
Then the service class only adds line numbers where errors where detected to linesInError
but it only gets the InputStream, where it could need say the original file name. You can pass the name as another parameter, or use a wrapper class :
class CsvFile {
private String name;
private InputStream inputStream;
CsvFile(MultipartFile file) {
name = file.getOriginalFilename();
inputStream = file.getInputStream();
}
// public getters ...
}
and call
customerService.insert(customerForm.customer, new CsvFile(customerForm.csvFile), linesInError);
with no direct Spring dependancies

jax-rs and server name

I am still working on a jax-rs server, and I faced some new problems recently. I do not understand where I define the name of my webserver. I searched everything in my workspace, but couldn't find anything.
Let's roll out the problem a bit further:
I always reach my server's #GET method via http://XXXXXX.XXXXX.XXX-XXXXXXX.de/android/
This is the structure of my server class:
#Path("/users")
public class UserResource {
Connection dbconn = null;
public UserResource() {
userIds = new ArrayList<Integer>();
userIds.add(1);
userIds.add(2);
userIds.add(3);
}
#GET
#Path("/login/{id}")
#Consumes("application/xml")
public StreamingOutput getTests(#PathParam("id") int id, InputStream is) {
return new StreamingOutput() {
public void write(OutputStream outputStream) throws IOException,
WebApplicationException {
getTests(outputStream);
}
};
}
As you see, the path of my class is "/users", and the path of the #GET method is "/login/1" (for example id = 1). Now I tried to call the method via
http://XXXXXX.XXXXX.XXX-XXXXXXX.de/android/users/login/1
But this does not work. I get an error (unknown source). And my error.log says that it couldn't find the resource at
http://XXXXXX.XXXXX.XXX-XXXXXXX.de/users/users/login/1
My 1st question: Where does the double "/users" come from? I have no idea. When I leave away the "/users" in my request url, there will be only 1 "/users" in the error.log, but still the resource is not found.
And there is another thing I did not find out yet: How do I change the name of my service? Atm, it's "android", but how could I change this? I searched my whole workspace, found "android" in my pom.xml, but when i change it to, let's say "testandroid", upload everything, build and run the server, the name is still android. No idea why this is the case.
Thx for your help guys!
EDIT:
This is my "main" class:
package com.restfully.services;
import javax.ws.rs.core.Application;
import java.util.HashSet;
import java.util.Set;
public class ServerApplication extends Application {
private Set<Object> singletons = new HashSet<Object>();
private Set<Class<?>> empty = new HashSet<Class<?>>();
public ServerApplication() {
singletons.add(new UserResource());
}
#Override
public Set<Class<?>> getClasses() {
return empty;
}
#Override
public Set<Object> getSingletons() {
return singletons;
}
}
I am using Eclipse and Maven. The application runs on a jetty-server. If you could use any further information, let me know.
You can look in the following places
Pom.xml file for context root the following entry;
<configuration>
<context>yourWarName</context>
</configuration>
Using Netbeans check Run Category context path under project properties.
Context Path: /yourWarName
Have a look in your web.xml as well.
When using jax-rs you normally define a config class;
#ApplicationPath("resources")
public class RestConfig extends Application{
}
From there you define your other paths;
#Stateless
#Path("/login")
public class LoginResource
public Response login(Credentials credentials) {
Credentials result = this.loginService.login(credentials);
return Response.status(Response.Status.OK).entity(result).build();
}
The path to the following is: http://domain.com/MyApp/resources/login
where MyApp is the context root.
It might be that there is a path specified in config or root with the name users that you are getting the double users.

Categories