Wednesday, July 2, 2014

Notes on Publishing Equinox OSGi services as JAX-WS services using Apache CXF with WS-Security* support

... This are my notes in case I forget during the process ...

Goal: Publish Liferay services as JAX-WS API
=================================

Problem: JAX-WS is supported by each app server differently.

Imagine having:
* JAX-WS 2.0, 2.1 and 2.2
* GlassFish, Tomcat + TCat, JBoss, IBM WAS, Oracle WLS
* Several version of each app server/container are certified / supported (see https://www.liferay.com/services/support/support-matrix)

To expose Liferay services we would need to tune each app server, version and write specific deployment instructions.

Solution: Isolate JAX-WS implementation using ClassLoaders.

Here comes OSGi which can load whatever JAR files we want and separate the implementation from app server class loaders.

------------------------------------------------------------

Decisions:
* use OSGi

------------------------------------------------------------

Problem: Find the best JAX-WS framework for OSGi

Not all JAX-WS frameworks can work correctly in OSGi and can be loaded by OSGi correctly.

+ we need WS-Security* enabled JAX-WS framework

Solution: Small analysis, CXF wins

Most common JAX-WS frameworks are (alphabetically):
* Axis2
* CXF
* Metro (reference implementation)

Prepared for OSGi are:
1, CXF
2, Axis2

Both support WS-Security into some degree, CXF seems to be better, has full JUnit security tests.

Google popularity:
axis2 problem: 445 000 hits
* cxf problem:  594 000 hits
jax-ws problem: 4 200 000 hits :D

Trends:

------------------------------------------------------------

Decisions:
* use OSGi
* use CXF

------------------------------------------------------------

Problem: CXF doesn't work in other OSGi implementations

CXF 2.x works correctly with Apache Karaf + Apache Aries for Blueprint.

CXF 2.x declares many extension points using Blueprint XML namespaces, if Apache Aries is not available, CXF fails to load.

See: https://issues.apache.org/jira/browse/CXF-5703

Solution: Use CXF 3.0 Beta

It's rather a workaround, CXF still tries to register custom namespaces using Aries.

If Aries is not found then continue, but CXF extensions are not available from Blueprint XML:

Jul 02, 2014 2:48:44 PM org.apache.cxf.bus.blueprint.NamespaceHandlerRegisterer register
WARNING: Aries Blueprint packages not available. So namespaces will not be registered
java.lang.NoClassDefFoundError: org/apache/aries/blueprint/NamespaceHandler
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:800)
at org.eclipse.osgi.internal.baseadaptor.DefaultClassLoader.defineClass(DefaultClassLoader.java:188)
at org.eclipse.osgi.baseadaptor.loader.ClasspathManager.defineClassHoldingLock(ClasspathManager.java:638)
at org.eclipse.osgi.baseadaptor.loader.ClasspathManager.defineClass(ClasspathManager.java:613)
at org.eclipse.osgi.baseadaptor.loader.ClasspathManager.findClassImpl(ClasspathManager.java:574)
at org.eclipse.osgi.baseadaptor.loader.ClasspathManager.findLocalClassImpl(ClasspathManager.java:492)
at org.eclipse.osgi.baseadaptor.loader.ClasspathManager.findLocalClass(ClasspathManager.java:465)
at org.eclipse.osgi.internal.baseadaptor.DefaultClassLoader.findLocalClass(DefaultClassLoader.java:216)
at org.eclipse.osgi.internal.loader.BundleLoader.findLocalClass(BundleLoader.java:395)
at org.eclipse.osgi.internal.loader.BundleLoader.findClassInternal(BundleLoader.java:464)
at org.eclipse.osgi.internal.loader.BundleLoader.findClass(BundleLoader.java:421)
at org.eclipse.osgi.internal.loader.BundleLoader.findClass(BundleLoader.java:412)


=>  CXF must be configured programatically.

------------------------------------------------------------

Decisions:
* use OSGi
* use CXF
* use CXF version >= 3.0
* configure CXF programatically

------------------------------------------------------------

Problem: Some JAR files are not OSGi bundles, installation of CXF into container is complicated


Worth noting - works only on my environment :D

------------------------------------------------------------

Decisions:
* use OSGi
* use CXF
* use CXF version >= 3.0
* configure CXF programatically
* use install scripts

------------------------------------------------------------

Problem: Unresolved optional dependencies, that are not optional :)

CXF with JAX-WS and WS-Security = many bundles.

Because WS-Security is optional, it's not loaded when it's missing / installed later.

To resolve all bundles it takes non-trivial effort that ends with unresolved optional Import package references.

Solution: Trial-error bundle loading to satisfy all optional import packages

Recommendation: 
1, use "diag" command from gogo console!
2, enable debug in Equinox to see what has been imported and what keeps unresolved
osgi.debug=/opt/liferay.git/bundles/portal-ext.properties
The file content:
org.eclipse.osgi/resolver/debug=true
org.eclipse.osgi/resolver/wiring=true
org.eclipse.osgi/resolver/imports=true
org.eclipse.osgi/resolver/requires=true
org.eclipse.osgi/resolver/generics=true
org.eclipse.osgi/resolver/uses=true
org.eclipse.osgi/resolver/cycles=true
Result: (for now) gogo-console script

Should be: a one-time install package like Apache Karaf features

------------------------------------------------------------

Decisions:
* use OSGi
* use CXF
* use CXF version >= 3.0
* configure CXF programatically
* use install scripts

------------------------------------------------------------

Problem: To declare security used by JAX-WS we need WS-Policy but we don't have WSDL and  existing OSGi services doesn't have JAX-WS @Policy annotations.

Solution: Use External Policy Attachments for WS-Policy

Let's configure CXF to map external policies onto JAX-WS services.

------------------------------------------------------------

Decisions:
* use OSGi
* use CXF
* use CXF version >= 3.0
* configure CXF programatically
* use install scripts
* use WS-Policy external attachments

------------------------------------------------------------

Problem: CXF doesn't support WS-Policy external attachments to be configured programatically

CXF's ExternalAttachmentProvider class doesn't have public constructor and is created only as Spring bean (called by Apache Aries).

Solution: Create a OSGi fragment and fix the provider so that it can be used outside the package.

------------------------------------------------------------

Decisions:
* use OSGi
* use CXF
* use CXF version >= 3.0
* configure CXF programatically
* use install scripts
* use WS-Policy external attachments
* use a OSGi fragment to fix ExternalAttachmentsProvider

------------------------------------------------------------

Prototype: How dynamically apply external policy XMLs to existing services?

1, Configure external policy file using patched ExternalAttachmentsProvider
2, Enrich default external policy to be able to dynamically match services

=>  Create CXF extension
* CXF extension description META-INF/cxf/bus-extensions.txt
* + DomainExpression builder prototype that is able to apply policies on the service / service parts

------------------------------------------------------------

Decisions:
* use OSGi
* use CXF
* use CXF version >= 3.0
* configure CXF programatically
* use install scripts
* use WS-Policy external attachments
* use a OSGi fragment to fix ExternalAttachmentsProvider
* use a custom CXF extension to dynamically map services to external policy file

------------------------------------------------------------

Problem: OSGi services are not complatible with JAX-WS

Services  don't have JAX-WS @WebService annotation which is required by JAX-WS

Solution: Use Javassist to generate new interface with @WebService annotation and dynamic proxy to apply the annotated interfaces on the existing service

------------------------------------------------------------

Decisions:
* use OSGi
* use CXF
* use CXF version >= 3.0
* configure CXF programatically
* use install scripts
* use WS-Policy external attachments
* use a OSGi fragment to fix ExternalAttachmentsProvider
* use a custom CXF extension to dynamically map services to external policy file
* use Javassist and dynamic proxies to add @WebService annotation

------------------------------------------------------------

Problem: OSGi services are not JAXB friendly.

Types that services use may not be easily marshallable, but OSGi services doesn't support JAXB annotation.

JAXB is not able to generate XSD into WSDL to correctly describe types and interfaces used in the methods.


Solution: Generate type wrappers so that JAXB is able to marshall interfaces and unknown types.

The type wrappers are used instead of original types. When the type cannot be wrapped I ignored the type => the whole web method.


------------------------------------------------------------

Decisions:
* use OSGi
* use CXF
* use CXF version >= 3.0
* configure CXF programatically
* use install scripts
* use WS-Policy external attachments
* use a OSGi fragment to fix ExternalAttachmentsProvider
* use a custom CXF extension to dynamically map services to external policy file
* use Javassist and dynamic proxies to add @WebService annotation
* use Javassist to generate type wrappers for unmarshallable types

------------------------------------------------------------

Problem: CXF is not able to generate correct WSDL from java proxies

The whole solution with dynamic proxies is bad. CXF doesn't want to create WSDL based on the annotation interface. It inspects the actual class and defined methods ... doesn't work with dynamic proxies.

Solution: Throw out dynamic proxies and generate services wrapper class using Javassist.

The prototype is semi-working, see further.

------------------------------------------------------------

Decisions:
* use OSGi
* use CXF
* use CXF version >= 3.0
* configure CXF programatically
* use install scripts
* use WS-Policy external attachments
* use a OSGi fragment to fix ExternalAttachmentsProvider
* use a custom CXF extension to dynamically map services to external policy file
* use Javassist and dynamic proxies to add @WebService annotation
* use Javassist to generate type wrappers for unmarshallable types
* don't use dynamic proxies and generate wrappers for both services and unmarshallable types

------------------------------------------------------------

Problem: ClassLoading problems

* ClassNotFoundException ... generated classes are loaded by a different classloader
* ClassCastException ... bundle classes are loaded twice, once inside generated classes and once using bundle classloader

Solution: We need custom ClassPool and class loader for defining classes that see all services & bundles classes.

Liferay AggregateClassLoader with WeekReferences is the perfect fit for bundle classloaders that will be dynamically added and removed when OSGi service is registered / removed.

We also need strictly delegating ClassLoader for instantiating CtClasses, because we don't override existing classes.

=> JaxWsClassPool with custom ClassLoader having AggregateClassLoader as parent.

This way generated classses are stored in the custom ClassLoader, existing classes are found using parent classloader - AggregateClassLoader which holds references to bundles' ClassLoaders.

Problem: method.getClass().getPackage() == null for generated classes???

CtClass.toClass() only creates the class object, but not package object. We can use javassist.Loader class loader to generate the package. See https://community.jboss.org/thread/93814?start=0&tstart=0

But, CtClass.toClass() calls directly ClassLoader.define() methods using reflection API => javassist.Loader.findClass() that creates the package object is not called => package is not created.

Solution:
1. We cannot call CtClass.toClass() directly, but ClassPool.getClassLoader().findClass(CtClass.getName()).
2, We need our strict loading ClassLoader to be javassist.Loader:

JaxWsClassPool -> DelegatingLoader (extends javasssist.loader) -> AggregatedClassLoader (references bundles class loaders)

Problem: Primitive conversion: auto-boxing doesn't work using Javassist compiler.

Solution: We need to correctly cast types and convert them so that they match method signatures :/

Ends up in another working prototype

------------------------------------------------------------

Decisions:
* use OSGi
* use CXF
* use CXF version >= 3.0
* configure CXF programatically
* use install scripts
* use WS-Policy external attachments
* use a OSGi fragment to fix ExternalAttachmentsProvider
* use a custom CXF extension to dynamically map services to external policy file
* use Javassist and dynamic proxies to add @WebService annotation
* use Javassist to generate type wrappers for unmarshallable types
* don't use dynamic proxies and generate wrappers for both services and unmarshallable types
* use custom class pool with custom class loading architecture
* use type casting for primitives, auto-box primitives manually

------------------------------------------------------------

Problem: Type wrappers with auto-boxed primitives cannot be used as original type instances.

There is a clash between a type wrapper implementing original type (model class) and the original type.

Having primitive field "int", the type Wrapper has Integer => getInt() must return Integer for JAXB and "int" to satisfy the implementation.

Problem: Unmarshallable types inside collections and maps cannot be handled by JAXB.

Having an ArrayList with Objects, we don't wrap instances of the objects -> JAXB doesn't know how to marshall the objects, because we don't convert them into type wrappers.

Solution: 
1, Avoid type wrappers conversion at all, delegate types conversion on JAXB.
2, Generate Type Wrappers as now, but implement the type <-> type wrapper conversion inside custom XMLAdapters.
2,  Dynamically generate the XMLAdapter classes for types that cannot be marshalled.
3,  Use @XmlJavaTypeAdapter on package level to define supported type conversion

This way the type can be converted in any situation.

... to be implemented

No comments:

Post a Comment