Monday, March 22, 2010

Parsing a Jar for annotated POJOs

How to parse a given jar file for Annotated POJOs without extracting the jar and without loading the classes?

After trying few open source solutions, I got a solution in eclipse newsgroup. See this thread.

Now let me explain step-by-step procedure to identify annotated pojos inside a Jar.

Have these imports in your class:

import org.apache.cxf.common.classloader.ClassLoaderUtils;
import org.eclipse.jdt.core.ToolFactory;
import org.eclipse.jdt.core.util.IAnnotation;
import org.eclipse.jdt.core.util.IClassFileAttribute;
import org.eclipse.jdt.core.util.IClassFileReader;
import org.eclipse.jdt.core.util.IRuntimeInvisibleAnnotationsAttribute;
import org.eclipse.jdt.core.util.IRuntimeVisibleAnnotationsAttribute;
import org.eclipse.jdt.internal.core.util.ClassFileReader;


Step1: Get the local file system URL of the Jar:

java.util.jar.JarFile jarFile =
new java.util.jar.JarFile( < jarURL.getPath() > );


Step2: Have an ArrayList populated with the required annotations. Eg:

List reqAnnotations = new ArrayList();
reqAnnotations .add("javax.jws.WebService");
reqAnnotations .add("javax.jws.WebMethod");


Step3: Get the list of annotated POJOs inside the Jar




private List findAnnotatedPOJOsInJar (JarFile jarFile){
List annotatedPOJOsList = new ArrayList();
Enumeration jarEntries = jarFile.entries();
while (jarEntries .hasMoreElements()) {
JarEntry jarEntry = jarEntries .nextElement();
if (jarEntry.isDirectory())
continue;
String entryName = entry.getName();
// Ignore files other than
if (!jarEntryName.endsWith(".class"))
continue;

ClassFileReader classReader = (ClassFileReader)
ToolFactory.createDefaultClassFileReader(jarFile
.getName(), jarEntry.getName(), IClassFileReader.ALL);

IClassFileAttribute[] classAttrs = classReader.getAttributes();

IRuntimeVisibleAnnotationsAttribute visibleAnnotations = null;
IRuntimeInvisibleAnnotationsAttribute invisibleAnnotations = null;
List annotationsOfThisClass = new ArrayList();

for (int i = 0; i < classAttrs.length; i++) {
IClassFileAttribute attr = attributes[i];
if (attr instanceof IRuntimeVisibleAnnotationsAttribute) {
visibleAnnotations = (IRuntimeVisibleAnnotationsAttribute) attr;
IAnnotation[] annotations = visibleAnnotations.getAnnotations();
populateAnnotationsList(annotationsOfThisClass , annotations);
} else if (attr instanceof IRuntimeInvisibleAnnotationsAttribute) {
invisibleAnnotations = (IRuntimeInvisibleAnnotationsAttribute) attr;
IAnnotation[] annotations = invisibleAnnotations.getAnnotations();
populateAnnotationsList(annotationsOfThisClass , annotations);
}
}

if (annotationsOfThisClass.isEmpty())
continue;
if (hasRequiredAnnotation(annotationsOfThisClass))
annotatedPOJOsList.add(entry);
}
return annotatedPOJOsList;
}

private void populateAnnotationsList (List annotationsOfThisClass, IAnnotation[] annotations) {
for (int i = 0; i < annotations.length; i++) {
IAnnotation iAnnotation = annotations[i];
char[] typeName = iAnnotation.getTypeName();
String annotation = new String(typeName).substring(1, typeName.length - 1).replace('/', '.');
annotationsOfThisClass.add(annotation);
}
}

private boolean hasRequiredAnnotation (List annotationsOfThisClass) {
for (String string : annotationsOfThisClass) {
if (reqAnnotations.contains(string))
return true;
}
return false;
}


Step4: You now have the List of JarEntries which are the POJOs with annotations.
From this list, you need to extract the class names from each of the entry.
Each entry will be name like com/sample/Abc. But we need the fully qualified class names:


List annotatedClassNames = ArrayList();
for (JarEntry jarEntry : annotatedPOJOsList) {
String entryName = jarEntry.getName();
int lastIndexOfSlash = entryName.lastIndexOf('/');
int indexOfDot = entryName.indexOf('.');
// get the package name of this class file
String packageName = null;
try {
packageName = entryName.substring(0, lastIndexOfSlash).
replace('/', '.');
} catch (StringIndexOutOfBoundsException e) {
// No package
packageName = "";
}
// get the class name
String className = entryName.substring(
lastIndexOfSlash + 1, indexOfDot);

String completeClassName = packageName.equals("") ?
className : packageName + "." + className;
annotatedClassNames.add(completeClassName);
}


So, using JDT's ClassFileReader, we are able to find pojos with annotations without extracting the jar, without loading the classes inside the jar. It is nice right :-).

Tuesday, March 2, 2010

Eclipse WTP Web Service Wizard tweaks

If you are planning to create your custom Web Service solution, I would suggest to check out the Web Services project which is a sub-project in the Eclipse WTP Top-Level Project. We can leverage many features and standards which are already built-in by WTP by extending WTP Web Service wizard to provide our custom Web Services solution. Though it has few problems/bugs, we can live with them and ask WTP guys to patch in their next releases :-)

I will share here a few tweaks and customizations you can do with the Eclipse Web Service wizard and few problems that exist as of WTP 3.1 release.

Following links could be useful if you are new to this:
> Contributing Web Service Runtime in WTP
> Consuming Web service using Web Service Client
> New Help for Old Friends III

Firstly, some basic pieces you need to know to contribute to Eclipse Web Service Wizard:

We can contribute Web Service Type using the extension point org.eclipse.jst.ws.consumption.ui.wsImpl.

Eg:
<extension point="org.eclipse.jst.ws.consumption.ui.wsImpl">
<webserviceimpl id="org.sample.contribution" label="My Sample Web Service" resourcetypemetadata="File IResource CompilationUnit" extensionmetadata=".extn" objectselectionwidget="com.sample.MySelectionWidget">
</webserviceimpl>
</extension>

Each such contribution will add two entries to the Web Service type combo on the Web Service wizard: Bottom Up My Sample Web Service and Top Down My Sample Web Service.

The extensions of the artifacts using which you would like to generate Web Service should be mentioned for the extensionMetadata attribute, separated by spaces.
For example, if you mention .abc as value for this attribute; when you right-click on an artifact in your workspace named sample.abc, and launch Web Service wizard, you would get a callback on in your transformer with TreeSelection object containing sample.abc , so that you can modify/perform some operation. See transformer.

objectSelectionWidget attribute takes the id of the extension point:org.eclipse.jst.ws.consumption.ui.objectSelectionWidget.

Below is an example for this extension point:
<extension point="org.eclipse.jst.ws.consumption.ui.objectSelectionWidget">
<objectSelectionWidget
class="com.sample.MySelectionWidget"
external_modify="true"
id="com.sample.MySelectionWidget"
transformer="com.sample.Transformer">
</objectSelectionWidget>
</extension>


This class com.sample.MySampleWidget should extend AbstractObjectSelectionWidget and/or implement IObjectSelectionWidget.

This class needs to implement the following methods:

public WidgetDataEvents addControls(Composite parent, Listener statusListener):
UI code you need to show up when user clicks on 'Browse' buttom for Service implementation should be written here. This method can return 'this' (the current object).

public void setInitialSelection(IStructuredSelection initialSelection)
:
Suppose you have selected Project1/Folder1/abc.xxx as your service implementation, which is
already populated on the Web Service Wizard first page :







initialSelection parameter holds the the value of the Service Implementation which can be used to populate your UI (provided by addControls() )

public IStructuredSelection getObjectSelection()
This method should return a StructuredSelection object which holds the service implementation.
Tweak 1:
Optionally, you can also send some more values in this StructuredSelection. This will be useful if you want to get multiple inputs from the user from the MySelectionWidget's UI.

public IStatus validateSelection(IStructuredSelection objectSelection)
You can return Status.OK_STATUS or Status.CANCEL_STATUS depending on your logic.

public IProject getProject()
Depending on the service implementation selected or depending on your logic you can return an appropriate Project in the workspace. This Project will be set for the Service Project.
If we don't override this method or return null, by default first project in the workspace will be selected.

public String getObjectSelectionDisplayableString()
The string returned from this method will be displayed as the Service implementation. You can return any string depending on you logic (eg., your custom file system URL).

public Point getWidgetSize()
eg: return new Point(400, 300);

public boolean validate(String s)
This method gets callback whenever text in the Service Implementation changes. Eg: When user types in, or when he selects from MySelectionWidget. Returning false will error out the wizard saying "The service implementation selected is invalid."


The attribute transformer of takes the fully qualified path of the class that implements org.eclipse.wst.command.internal.env.core.data.Transformer.

Using Transformer, we can transform the given object from one class to another. For example, you can return a StructuredSelection consisting the artifact selected in the project explorer. This would be passed to the MySelectionWidget.setInitialSelection().

This class need to implement public Object transform(Object value):
The parameter value contains the StructuredSelection returned from MySelectionWidget.getObjectSelection().
You can return whole new StructuredSelection() object: new StructuredSelection(< selection >);

Tweak 2 and Tweak 3 :
Setting your contribution to Service Type and Client type as defaults and Setting Service slider and Client Slider to a particular stage (eg: Develop stage):

Invoke the below method from your plugin's start() method:

private void setWebServiceScenarioDefaults() {
ScenarioContext context = WebServicePlugin.getInstance().getScenarioContext();

context.setWebServiceType( < Id of webServiceImpl > );
context.setGenerateWebService(ScenarioContext.WS_DEVELOP);

context.setClientWebServiceType( < Id of webServiceClientImpl > );
context.setGenerateClient(ScenarioContext.WS_DEVELOP);
}

Here, Id of webServiceImpl should be the Service type number / id of webServiceImpl (see extension point="org.eclipse.jst.ws.consumption.ui.wsImpl")
Eg: 0/
org.sample.contribution for Bottom Up My Sample Web Service or 1/org.sample.contribution for Top Down My Sample Web Service.

Id of webServiceClientImpl
should be the id of the webServiceClientImpl (see Extension point "org.eclipse.jst.ws.consumption.ui.wsClientImpl")
Eg: org.sample.client.contribution

























Tweak 4: Validating service project selection:

< extension point="org.eclipse.jst.ws.consumption.ui.serviceRuntimes" >
has an attribute runtimeChecker which takes fully qualified class name which extends org.eclipse.wst.ws.internal.wsrt.AbstractWebServiceRuntimeChecker.

This class can implement 2 methods:

public IStatus checkRuntimeCompatibility(String serverTypeId, String serverInstanceId,
String projectName, String projectTypeId, String earProjectName) {
return Status.OK_STATUS;
}

public IStatus checkServiceClientCompatibility(boolean serviceNeedEAR,
String serviceEARName, String serviceProjectName,
boolean clientNeedEAR, String clientEARName,
String clientProjectName) {
return Status.OK_STATUS;
}

Here, we can validate based on the
serverTypeId, serverInstanceId, projectName, projectTypeId and earProjectName and return Status.OK_STATUS or Status.CANCEL_STATUS.

Some of the usability issues with WTP Web Services wizard:
  • There are few components on the Web Service Wizard first page on which do not have any control:
    1. Cannot change the Title and message when the Web Service Type selection changes (Eg: Select a Service implementation)
    2. Cannot change the names of the Bottom Up My Sample Service and Top Down My Sample Web Service to something like Java to WSDL Web Service and WSDL to Java Web Service.
    3. Cannot hook in custom dialogs for the Top Down Scenario for "Select Service Implementation" page (on clicking Browse button). This always opens up the WSDL chooser dialog. Suppose I need to start with an artifact other than WSDL for Top Down case, there is no option to customize it.
    4. Even if we do not need any Server, cannot disable the Server hyperlink.
    5. The checkboxes 'Publish the Web Service' and 'Monitor the Web Service' cannot be disabled or removed. The 'Publish Web service' page always comes up even when the 'Publish the Web Service' checkbox is not selected. See this bug for more details. Moreover if the the checkbox 'Monitor the Web Service' is selected, an exception would be thrown later in the wizard, if do not mention any server.
    6. If user selects any other project by clicking on the 'Service Project' link, this will create an EAR project as well. (This can be avoided by setting the Service slider to develop stage. See Tweak 3)
    7. 'Undo' operation for client scenario might also be required, because, if the user selects the client scale to 'Test', extra pages will be added to the wizard. This is because, once all your pages have been served to the wizard, extra pages (Publish Web Service page, Start server page, etc) will be added to the wizard. Since you get final callback only when Next is clicked on your last page and not on the Finish of the wizard, if you generate some artifacts here, and user cancels the wizard while he is navigating the extra pages served by WTP, you need to Undo the artifact generation.
    8. The whole Web Serice Client section on the first page is not needed at all, as this doesn't generate anything specific to the selected Client type while generating a Service type.
  • 'Web Service Publication' page will be added at the end of the wizard and we cannot be disable this.
  • There is no way to get callback when user clicks finish on the 'Web Service Publication' page. But, If user clicks Finish on one of the pages you serve, you do get callback on your command. It would be better if we can get callback on Finish of the wizard in all cases. This would avoid the overhead of handing Undo operations.


Few bugs which I have raised in bugzilla for the above mentioned points:
  • 295695:In Web Service Wizard, Server field should be cleared if Web Service type doesn't need a Server
  • 296430: Null Pointer Exception occurs when Web Service Wizard is launched with a resource from Java Project
  • 304805: Need support to set custom error/info message on the Web Service Wizard first page
  • 304808: Disable Web Service Client section on the Web Service wizard.
  • 280447: Need support for disabling few stages of webservice in the service side




Commands ( those that extend org.eclipse.wst.common.frameworks.datamodel.AbstractDataModelOperation) can be plugged in to your framework to do some validation and resource/artifact generation.
I will discuss this in my next blog entry...