Thursday, April 11, 2013
Package by features
Wednesday, May 2, 2012
Configurable, and modular Java classes using Annotations and Enumerations
In todays blog post, I'm showing how I implemented highly configurable, and modular classes with the help of annotations, and enumerations. From now on, let's call these modular classes "modules". Let's suppose we have the following architectural problem:
- There is a need for modules that are highly configurable.
- The modules implement the same interface.
- The configuration parameters vary.
- Some configuration parameters are required.
- Some configuration parameters have a default value.
- A configuration parameter should have an explanation for the enduser.
- It should also be easy to add new modules.
UML Class Diagram
ModuleFactory and ModuleFactoryImpl
The module factory is responsible for creating a configured module. It takes the class name, and Properties object as parameters. I'm using a class name here instead of a class type, because the list of supported modules are stored in the database as names. The Properties object contains the configuration for the module to be created.
ModuleType
This enumeration is basically a list of supported types by the factory. When a new module is implemented, it should be added to this enumeration, otherwise the factory doesn't know how to translate the class name to the actual class. I tried to find a way to avoid the need of this enumeration, like searching in the class path for classes that correspond to the name. Unfortunately, this is too complex to achieve without adding a lot of boilerplate code. If you find a better way to solve this problem, please let me know in the comments.
Module
This interface formalizes the methods implemented by all modules. Their parameters are defined in an enumeration of type ModuleParameter. Every module should have a corresponding ModuleParameter enumeration. An example implementation for this demo is called "ExampleModule", and it uses "ExampleModuleParameter" to define its configuration.
ModuleParameter
This interface specifies the required methods for every subclass enumeration implementation. The enumeration is basically a list of configuration parameters for a module. Per configuration parameter, it also defines: if the parameter is required, the default value, and a brief explanation. An example implementaton for this demo is called "ExampleModuleParameter", and it has two configuration parameters.
The Implementation
ModuleParameter
package com.javaeenotes.modules;
// This interface enforces required methods for the parameter enumerator.
public interface ModuleParameter {
boolean isRequired();
String getDefaultValue();
String getExplanation();
}
This interface defines the methods used to retrieve the properties of the configuration parameter.
ExampleModuleParameter
package com.javaeenotes.modules;
// This enum class contains the parameters used to configure a module.
public enum ExampleModuleParameter implements ModuleParameter {
PARAM1("param1", true, "DEFAULT", "This is parameter 1."),
PARAM2("param2", false, null, "This is parameter 2.");
// Code below is boiler plate code. It is not possible to move the
// code to an abstract class, because enumerators are not allowed
// to extend classes.
private final String name;
private final boolean isRequired;
private final String defaultValue;
private final String explanation;
private ExampleModuleParameter(
String name,
boolean isRequired,
String defaultValue,
String explanation) {
this.name = name;
this.isRequired = isRequired;
this.defaultValue = defaultValue;
this.explanation = explanation;
}
@Override
public String toString() {
return name;
}
@Override
public boolean isRequired() {
return isRequired;
}
@Override
public String getDefaultValue() {
return defaultValue;
}
@Override
public String getExplanation() {
return explanation;
}
}
Everything you should know about the specific parameters is defined here, and nowhere else (high cohesion)! It forces the developer who is adding new parameters to define the extra properties as required.
I tried to eliminate the boiler plate code, by moving them to a shared abstract enumeration class, but that is not possible. Now we have to copy the same code for every new "ModuleParameter" implementation. Is there a better solution for this?
Module
package com.javaeenotes.modules;
public interface Module {
void doBusinessStuff();
}
Just the interface of a module, containing a common method.
ExampleModule
package com.javaeenotes.modules;
// Annotate this module with parameter class that contains the
// configurable parameters for this module.
@ParameterClass(parameterClass = ExampleModuleParameter.class)
public class ExampleModule implements Module {
private String param1;
private String param2;
@Override
public void doBusinessStuff() {
System.out.println("Hello, I am "
+ this.getClass().getSimpleName()
+ ". I am configured with param1='" + param1
+ "', and param2='" + param2 + "'.");
}
public void setParam1(String param1) {
this.param1 = param1;
}
public void setParam2(String param2) {
this.param2 = param2;
}
}
This is an example implementation of a module. As you can see, it's just a POJO, making it very easy to write new ones. The class is linked to its parameter class using the annotation placed just before the class definition. The attributes and setters are important here. The names must correspond with the names defined in the parameter class.
ParameterClass
package com.javaeenotes.modules;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// Only usable on classes.
@Target(ElementType.TYPE)
// Annotation must be available during runtime.
@Retention(RetentionPolicy.RUNTIME)
public @interface ParameterClass {
Class<? extends ModuleParameter> parameterClass();
}
This is the annotation, which links the module to its parameter class. The factory uses this annotation to find out which configuration values to set.
ModuleType
package com.javaeenotes.modules;
// This is basically a list of supported modules by the factory.
public enum ModuleType {
EXAMPLE_MODULE(ExampleModule.class);
private Class<? extends Module> moduleClass;
private ModuleType(Class<? extends Module> moduleClass) {
this.moduleClass = moduleClass;
}
public Class<? extends Module> getModuleClass() {
return moduleClass;
}
}
This enumeration is a list of supported modules. The purpose of this enumeration is to map the name to its class type. This information is required by the module factory to instantiate the correct module class.
ModuleFactory
package com.javaeenotes.modules;
import java.util.Properties;
public interface ModuleFactory {
Module getModule(String moduleName, Properties configuration)
throws Exception;
}
The interface of the module factory. It requires the class name, and a "Properties" object containing the configuration of the module.
ModuleFactoryImpl
package com.javaeenotes.modules;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
public class ModuleFactoryImpl implements ModuleFactory {
private Map<String, ModuleType> nameToModuleType;
public ModuleFactoryImpl() {
// The Map contains all the supported module types.
nameToModuleType = new HashMap<String, ModuleType>();
for (ModuleType moduleType : ModuleType.values()) {
nameToModuleType.put(
moduleType.getModuleClass().getSimpleName(),
moduleType);
}
}
@Override
public Module getModule(String moduleName, Properties configuration)
throws Exception {
if (nameToModuleType.containsKey(moduleName)) {
ModuleType moduleType = nameToModuleType.get(moduleName);
Module module = moduleType.getModuleClass().newInstance();
configureModule(module, configuration);
return module;
}
throw new IllegalArgumentException(
"Class not supported: " + moduleName);
}
private void configureModule(
Module module, Properties configuration)
throws Exception {
if (module.getClass().isAnnotationPresent(ParameterClass.class)) {
Map<String, Method> methodNameToMethod =
getMethodNameToMethod(module);
for (ModuleParameter param : getModuleParameters(module)) {
String configValue = configuration.getProperty(
param.toString());
// Set default value if configuration value is empty.
if (configValue.isEmpty()) {
configValue = param.getDefaultValue();
}
// Check if value is required.
if ((configValue == null || configValue.isEmpty())
&& param.isRequired()) {
throw new IllegalArgumentException(
"Configuration value missing for: "
+ param.toString());
}
// Set configuration value in module.
invokeParameterSetter(methodNameToMethod, module,
param.toString(), configValue);
}
}
}
private Map<String, Method> getMethodNameToMethod(Module module) {
Map<String, Method> methodNameToMethod =
new HashMap<String, Method>();
for (Method method : module.getClass().getMethods()) {
methodNameToMethod.put(method.getName(), method);
}
return methodNameToMethod;
}
private ModuleParameter[] getModuleParameters(Module module) {
return module.getClass()
.getAnnotation(ParameterClass.class)
.parameterClass().getEnumConstants();
}
private void invokeParameterSetter(
Map<String, Method> methodNameToMethod,
Module module, String paramName, String configValue)
throws Exception {
String setterMethodName = getSetterMethodName(paramName);
if (methodNameToMethod.containsKey(setterMethodName)) {
methodNameToMethod.get(setterMethodName).invoke(
module, configValue);
} else {
throw new IllegalArgumentException(
"No setter found for: " + paramName);
}
}
private String getSetterMethodName(String paramName) {
return "set" + Character.toUpperCase(paramName.charAt(0))
+ paramName.substring(1);
}
}
Using the name of the module class, this factory instantiates the correct class with the help of the "ModuleType" enumeration class. Then it tries to find out if this module requires configuration, by detecting the annotation. The annotation specifies the use of the "ExampleModuleParameter" enumeration for configuration. Using the enumeration, and the "Properties" object, it calls the setters with the configuration values. It also checks if a required configuration value is present. The result is a module with all required configuration values set.
A drawback is that the method name of the setters is restricted. It must be the same as the name of the parameter (excluding the "set" string), which is defined in the enumeration. A solution for this is to use annotations in the setter method definition to specify which configuration parameter it corresponds to. Using string literals to tie things together makes the code brittle.
Main
package com.javaeenotes.modules;
import java.util.Properties;
public class Main {
public static void main(String[] args) {
// Build the configuration properties.
Properties configuration = new Properties();
configuration.setProperty(
ExampleModuleParameter.PARAM1.toString(), "");
configuration.setProperty(
ExampleModuleParameter.PARAM2.toString(), "param2");
try {
// Get instance of configured module from factory.
Module module = new ModuleFactoryImpl()
.getModule("ExampleModule", configuration);
// Run the module.
module.doBusinessStuff();
} catch (Exception e) {
e.printStackTrace();
}
}
}
This class demonstrates the whole solution. If run, it prints: Hello, I am ExampleModule. I am configured with param1='DEFAULT', and param2='param2'.
Conclusion
As you can see, writing new modules is very easy by implementing just the two interfaces. All the configuration code is centralized in the factory implementation.
If you have ideas for improvements or better solutions, please don't hesitate to share them in the comments.
Monday, December 26, 2011
Annotated callback methods in custom class
We're implementing a simple message service framework to demonstrate how we can implement a framework that uses annotations, like JAX-WS. Using annotations minimizes boilerplate code, and promotes high cohesion of classes. It also promotes decoupling, because it prevents forcing the use of framework classes on clients as much as possible.
The messaging framework has one interface with a register method, which is used to register custom objects that handle incoming messages. The framework does not know what the type is of this object. It uses the reflection API to check for annotated methods, which tell the messaging framework to call these methods when a message has to be handled.
The message service framework supports two types of messages, which we define in as an enum type in MessageType.java:
package com.javaeenotes;
public enum MessageType {
NORMAL,
URGENT
}
The interface of the message service framework has a method to register custom client objects that will handle incoming messages. The second method of the interface is only included, so we can send messages through this framework. The interface of the message service framework is defined in MessageService.java:
package com.javaeenotes;
public interface MessageService {
void registerMessageHandler(Object object);
void sendMessage(String message, MessageType messageType)
throws Exception;
}
Now, we define our annotation. This annotation can be used by the client application to mark methods that will handle incoming messages generated by the message service framework. It also has a message type enum parameter, so the framework will only call this method if the type of the message matches the specified type parameter. The annotation is defined in MessageMethod.java:
package com.javaeenotes;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// Only usable on methods.
@Target(ElementType.METHOD)
// Annotation must be available during runtime.
@Retention(RetentionPolicy.RUNTIME)
public @interface MessageMethod {
MessageType messageType() default MessageType.NORMAL;
}
A client object will use the annotation to mark callback methods for specified message types. The example client handler has one method for both message types. An example client message handler class for this demo is defined as ExampleMessageHandler.java:
package com.javaeenotes;
public class ExampleMessageHandler {
@MessageMethod(messageType = MessageType.NORMAL)
public void handleNormalMessage(String message) {
System.out.print("NORMAL MESSAGE: " + message + "\n");
}
@MessageMethod(messageType = MessageType.URGENT)
public void handleUrgentMessage(String message) {
System.out.println("URGENT MESSAGE: " + message + "\n");
}
}
The most important class of this tutorial is: MessageServiceImpl.java, which you can find below. This is the implementation of our message service framework interface. This class is responsible for determining callback methods provided by the client, and call them when messages arrive.
package com.javaeenotes;
import java.lang.reflect.Method;
public class MessageServiceImpl implements MessageService {
private Object messageHandler;
@Override
public void registerMessageHandler(Object messageHandlerObject) {
messageHandler = messageHandlerObject;
}
@Override
public void sendMessage(String message, MessageType messageType)
throws Exception {
for (Method method : messageHandler.getClass().getMethods()) {
if (method.isAnnotationPresent(MessageMethod.class)) {
MessageMethod messageMethod
= method.getAnnotation(MessageMethod.class);
if (messageMethod.messageType() == MessageType.NORMAL
&& messageType == MessageType.NORMAL) {
method.invoke(messageHandler, message);
} else if (messageMethod.messageType()
== MessageType.URGENT
&& messageType == MessageType.URGENT) {
method.invoke(messageHandler, message);
}
}
}
}
}
Now, we only need a Main class to get this demonstration working. The class will create the example client object, and register it in the framework. Then, it will create two example messages, and pass them to the framework, which then will call the annotated callback methods of the example client object. The Main class is defined in: Main.java.
package com.javaeenotes;
public class Main {
public static void main(String[] args) {
// Create message service and register message handler.
MessageService messageService = new MessageServiceImpl();
messageService.registerMessageHandler(
new ExampleMessageHandler());
try {
// Sending test messages through the message service.
messageService.sendMessage("This is a normal message.",
MessageType.NORMAL);
messageService.sendMessage("This is an urgent message!",
MessageType.URGENT);
} catch (Exception e) {
e.printStackTrace();
}
}
}
If we run the Main class, we should get the following output:
NORMAL MESSAGE: This is a normal message. URGENT MESSAGE: This is an urgent message!
Tuesday, September 13, 2011
Extensible Factory Method implementation
One of my favourite design pattern is the Factory Method. The design pattern encapsulates object creation, i.e. the factory method decides which implementation you'll get. I often use a slightly different version of the pattern, which returns a specific implementation of an interface based on a name parameter. The supported names are defined as static constants in the factory class. This way, I decouple the actual implementation from its user. The only link to the actual implementation is the name constant.
The implementation I use has several problems. Everytime I add a new manufacturable class, I have to change the factory class by adding a new name constant to identify the new manufacturable. And I have to add nearly the same code to actually instantiate and return the new type by the factory, which violates the DRY-principle. This also makes the factory tightly coupled to the new type, which I want to avoid if possible.
Another problem I had in the past was that I use to same code in every Singleton class (Factory Methods are often Singletons). I solved this by using a Dependency Injection Framework like Google Guice or Spring. It's possible to mark singleton classes using these frameworks by annotations or configuration. The frameworks will then make sure that the marked classes are singletons during execution.
So, I felt a need to improve my implementation of the factory methods. I wanted the following properties for my factory methods:
- It should be easy to create new factories, without much (duplicate) code.
- It should be easy to add new manufacturables, without changing code in the factory class.
The result is the implementation I'll describe next. Before we go to the actual implementation, let's create an interface for the manufacturable classes, and two example implementation of the manufacturable classes.
The manufacturable interface:
package com.javaeenotes;
public interface MyManufacturable {
public String getName();
}
Two implementations of the manufacturable interface:
package com.javaeenotes;
public class MyManufacturableAImpl implements MyManufacturable {
@Override
public String getName() {
return getClass().getSimpleName();
}
}
package com.javaeenotes;
public class MyManufacturableBImpl implements MyManufacturable {
@Override
public String getName() {
return getClass().getSimpleName();
}
}
The abstract factory method class:
package com.javaeenotes;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public abstract class AbstractFactory<T> {
private static final String CONFIG_FILE_EXTENSION = ".conf";
private final String factoryName;
private final Map<String, Class<? extends T>> manufacturableClasses;
public AbstractFactory(String name) {
this.factoryName = name;
this.manufacturableClasses = new HashMap<String, Class<? extends T>>();
try {
loadClasses();
} catch (IOException ioException) {
throw new IllegalArgumentException(
"Error reading factory configuration file.",
ioException);
} catch (ClassNotFoundException classNotFoundException) {
throw new IllegalStateException(
"Could not find class for name in factory configuration file.",
classNotFoundException);
}
}
@SuppressWarnings("unchecked")
private void loadClasses() throws IOException, ClassNotFoundException {
BufferedReader configReader = null;
try {
configReader = new BufferedReader(new InputStreamReader(getClass()
.getResourceAsStream(factoryName + CONFIG_FILE_EXTENSION)));
String className;
while ((className = configReader.readLine()) != null) {
manufacturableClasses.put(
className,
(Class<? extends T>) Class.forName(className));
}
} finally {
if (configReader != null) {
configReader.close();
}
}
}
public Set<String> getNamesManufacturables() {
return manufacturableClasses.keySet();
}
public T getInstance(String nameManufacturable)
throws InstantiationException, IllegalAccessException {
return manufacturableClasses.get(nameManufacturable).newInstance();
}
}
The factory method class contains no hard references to our manufacturable implementation classes. Hey, there's even no reference to our manufacturable interface, because we use generics!
The factory is designed as a generic abstract class, which should be extended by every factory method class in the application. The manufacturables are defined in a configuration file in the class path, which has the same name as the factory. The configuration file has a .conf file extension, and should contain the fully qualified names of the manufacturable classes. Upon factory instantiation, the configuration file is read, and all supporting manufacturable classes are loaded.
Here is the example factory:
package com.javaeenotes;
public class MyFactory extends AbstractFactory<MyManufacturable> {
private static final String name = "MyFactory";
public MyFactory() {
super(name);
}
}
The factory configuration file we use to support our manufacturable classes (MyFactory.conf):
com.javaeenotes.MyManufacturableAImpl com.javaeenotes.MyManufacturableBImplWe can use the following main class, which prints the names of the manufacturables, as demonstration:
package com.javaeenotes;
public class Main {
public void run() {
MyFactory factory = new MyFactory();
for (String name : factory.getNamesManufacturables()) {
try {
MyManufacturable myManufacturable = factory.getInstance(name);
System.out.println(myManufacturable.getName());
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new Main().run();
}
}
Conclusion
The factory method class implementation makes adding new factories very simple. Every factory class just has to extend the abstract factory class, and call its constructor with the name of the factory. The whole factory class consists of just several lines!
Registering new manufacturable classes to the factory is also much easier. Just add the fully qualified name of the new class to the factory configuration file, and we're done!
This concludes this post. If you have any improvements, please don't hesitate to share them in the comments.
Thursday, May 12, 2011
Java Practices
http://www.javapractices.com
Definitely worth a read!
Saturday, April 2, 2011
Google Guice
ExampleInterface object = new ExampleImpl();
We are only interested in the interface, so we want to avoid having to know the real implementation class. We can use a Factory Method design pattern to achieve this. Using this design pattern, we centralize and hide the implementation class in the factory class. This enables us to vary the implementation if needed. This is particular useful, when using mock objects for testing the application. Using Factory Methods reduces coupling and increases modularity.
public class ExampleFactory() {
public ExampleInterface getInstance() {
return new ExampleImpl();
}
}
The factory is commonly implemented as a Singleton.
A better way to couple objects together, is to use Dependency Injections. In JEE, this is available in a container for container managed classes. In the example code below, the container will automatically create and set a resource instance of the desired class.
public class Client {
@Resource
private ExampleInterface instance;
}
In other classes where container Dependency Injection is not possible, we can pass the implementation to the client class by using its constructor.
public class Client {
private ExampleInterface instance;
public Client(ExampleInterface instance) {
this.instance = instance;
}
}
The problem here is, that we need the implementation class whenever we instantiate the client class. This ties our implementation class to every location where we instantiate the client class, which we really want to avoid.
In this blog post, we'll use Google Guice to leverage Dependency Injection in the rest of our code. In order to use Guice, we need a class that maps interfaces to their implementation. The class has to extend AbstractModule provided by Guice.
public class ExampleModule extends AbstractModule {
@Override
protected void configure() {
bind(ExampleInterface.class).to(ExampleImpl.class);
bind(AnotherClass.class).to(AnotherInterface.class);
}
}
Now, we can use the @Inject annotation in places, where we need the implementations of mapped interfaces. The class below shows how.
public class ExampleClient {
// Attribute Injection
@Inject
private ExampleInterface instance;
// Constructor Injection
@Inject
public ExampleClient(ExampleInterface inst1) {
...
}
// Method Injection
@Inject
public void ExampleMethod(ExampleInterface inst2) {
...
}
}
Now, when we instantiate the client class, we let Guice to instantiate it.
Injector injector = Guice.createInjector(new ExampleModule());
ExampleClient client = injector.getInstance(ExampleClient.class);
Guice will create and inject the dependencies before returning the client class.
We can also use the injector as a factory.
ExampleInterface instance = injector.getInstance(ExampleInterface.class);
A complete example application can be found below. Make sure the following JAR-files are included in your build path when compiling the example:
- guice-3.0.jar
- javax.inject.jar
- aopalliance.jar
ExampleClient.java
package com.javaeenotes;
import com.google.inject.Inject;
public class ExampleClient {
// Attribute Injection
@Inject
private ExampleInterface instance;
// Constructor Injection
@Inject
public ExampleClient(ExampleInterface inst1) {
inst1.method("Constructor is successfully injected.");
}
// Method Injection
@Inject
public void ExampleMethod(ExampleInterface inst2) {
inst2.method("Method is successfully injected.");
}
// Used to test Attribute Injection
public void testAttribute() {
instance.method("Attribute is successfully injected.");
}
}
ExampleImpl.java
package com.javaeenotes;
public class ExampleImpl implements ExampleInterface {
@Override
public void method(String s) {
System.out.println(s);
}
}
ExampleInterface.java
package com.javaeenotes;
public interface ExampleInterface {
public void method(String s);
}
ExampleModule
package com.javaeenotes;
import com.google.inject.AbstractModule;
public class ExampleModule extends AbstractModule {
@Override
protected void configure() {
bind(ExampleInterface.class).to(ExampleImpl.class);
}
}
GuiceDemo.java
package com.javaeenotes;
import com.google.inject.Guice;
import com.google.inject.Injector;
public class GuiceDemo {
public static void main(String[] args) {
Injector injector = Guice.createInjector(new ExampleModule());
System.out.println("Getting instance using injector as a factory.");
ExampleInterface instance = injector
.getInstance(ExampleInterface.class);
instance.method("Test instance returned by injector.");
System.out.println("Getting injected client.");
ExampleClient client = injector.getInstance(ExampleClient.class);
client.testAttribute();
}
}
Saturday, March 26, 2011
Law of Demeter (LoD)
The rule is formulated as: Only talk to your friends. A friend is defined as a variable a piece of code directly knows, ie. an attribute, a local variable or a method parameter.
Now, let's examine the following code:
public void do(ExampleClass c) {
String s = "Hello";
c.getSomething().passString(s);
}
In this example, the code is coupled to three classes: ExampleClass, String, and an unknown class returned by the getSomething() method. Only the first two classes are considered friends.
The code is calling (talking to) methods of two objects: the ExampleClass object and the unknown object.
According to the LoD rule, we don't want to talk to classes that are not our friends. This unknown object is not a friend of us, so we'd rather not call its methods.
We can improve the code using the Law of Demeter like this:
public void do(ExampleClass c) {
String s = "Hello";
c.passStringToSomething(s);
}
A new method is created in ExampleClass in order to take over the responsibility for passing a String to the unknown object. The improved example is coupled to two classes now.
Friday, September 17, 2010
Number of Sun Certified Enterprise Architects (SCEA) in the world
Argentina: 14
Australia: 107
Austria: 27
Belgium: 52
Brazil: 200
Canada: 435
China: 138
Denmark: 58
Finland: 45
France: 45
Germany: 276
Hong Kong: 146
India: 421
Italy: 46
Netherlands: 142
New Zealand: 28
Norway: 47
Sweden: 72
Switzerland:114
UK: 348
Uraguay: 1
USA: 2334
Monday, June 21, 2010
Sonar (Code Quality Management System)
- Architecture & Design
- Comments
- Duplications
- Coding rules
- Unit tests
- Potential bugs
- Complexity
More info: http://www.sonarsource.org/
Friday, May 14, 2010
DeMilitarized Zone (DMZ)
The area between the two firewalls is called the DeMilitarized Zone or DMZ. When an intruder manages to compromise the web server, he still has to find a way to circumvent the inner firewall to actually get to the internal network or data.
Here is an PDF-article that explains how to design a DMZ for your network. More information about DMZ can be found here at Wikipedia.
Saturday, April 10, 2010
Java web application project directory structure
/webproject/ (the project root directory)
/webproject/build/ (this directory is packed into a WAR-file)
/webproject/test/ (system wide test files)
/webproject/setup/ (configuration files for container or project)
/webproject/dist/ (contains the WAR file)
/webproject/lib/ (libraries/jars)
/webproject/src/
/webproject/src/java/ (Java sources)
/webproject/src/conf/ (configuration files for build process)
/webproject/src/test/ (unit tests)
/webproject/docs/ (documentation)
/webproject/web/ (the web root starts here)
/webproject/web/WEB-INF/
/webproject/web/WEB-INF/jspf/*.jspf (JSP fragment/include files)
/webproject/web/WEB-INF/lib/
/webproject/web/WEB-INF/tlds/
/webproject/web/WEB-INF/tags/
/webproject/web/WEB-INF/faces-config.xml
/webproject/web/WEB-INF/web.xml
/webproject/web/*.jsp
/webproject/web/*.html
/webproject/web/css/ (CSS-files)
/webproject/web/img/ (images)
/webproject/web/js/ (JavaScript files)
/webproject/LICENSE.txt
/webproject/README.txt
/webproject/build.xml (ANT build file)
In the future, I'll post the ANT build file, which is used to automate build and package processes.
