/*
* Copyright (C) 2005-2012 BetaCONCEPT Limited
*
* This file is part of Astroboa.
*
* Astroboa is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Astroboa is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Astroboa. If not, see <http://www.gnu.org/licenses/>.
*/
package org.betaconceptframework.astroboa.client.service;
import java.util.Properties;
import javax.naming.Context;
import javax.naming.InitialContext;
import org.apache.commons.lang.StringUtils;
import org.betaconceptframework.astroboa.api.model.exception.CmsException;
import org.betaconceptframework.astroboa.client.AstroboaClient;
import org.jboss.ejb.client.ContextSelector;
import org.jboss.ejb.client.EJBClientConfiguration;
import org.jboss.ejb.client.EJBClientContext;
import org.jboss.ejb.client.PropertiesBasedEJBClientConfiguration;
import org.jboss.ejb.client.remoting.ConfigBasedEJBClientContextSelector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract class containing common methods for remote services wrappers.
*
* This class assumes that access is done through JBoss JNDI and
* that remoteService's JNDI name is remote service's class name plus "/remote".
*
*
* @author Gregory Chomatas (gchomatas@betaconcept.com)
* @author Savvas Triantafyllou (striantafyllou@betaconcept.com)
*
*/
public abstract class AbstractClientServiceWrapper {
final static String DEFAULT_PORT = "4447";
final static String ASTROBOA_EAR_APPLICATION_NAME = "astroboa";
final static String ASTROBOA_EJB_MODULE_NAME = "astroboa-ejb3";
/*
* distinct-name : This is a JBoss AS7 specific name which can be optionally assigned to the deployments that are deployed on the server.
* More about the purpose and usage of this will be explained in a separate chapter. If a deployment doesn't use distinct-name then,
* use an empty string in the JNDI name, for distinct-name.
*/
final static String ASTROBOA_EAR_DEPLOYMENT_DISTINCT_NAME = "";
final Logger logger = LoggerFactory.getLogger(getClass());
private String serverHostNameOrIp;
private String port;
String serverHostNameOrIpAndPortDelimitedWithSemiColon;
AstroboaClient client;
boolean successfullyConnectedToRemoteService;
public AbstractClientServiceWrapper(AstroboaClient client, String serverHostNameAndorPortToConnect){
this.client = client;
initialize(serverHostNameAndorPortToConnect);
}
abstract void resetService();
abstract boolean loadService(boolean loadLocalService);
private void initialize(String serverHostNameAndorPortToConnect){
if (StringUtils.isBlank(serverHostNameAndorPortToConnect)){
//Set default URL if none provided
serverHostNameAndorPortToConnect = AstroboaClient.INTERNAL_CONNECTION+":"+DEFAULT_PORT;
}
else {
//Set port if not provided
if (!serverHostNameAndorPortToConnect.contains(":")){
serverHostNameAndorPortToConnect = serverHostNameAndorPortToConnect.concat(":"+DEFAULT_PORT);
}
else {
if (StringUtils.isBlank(StringUtils.substringAfterLast(serverHostNameAndorPortToConnect, ":"))){
serverHostNameAndorPortToConnect = serverHostNameAndorPortToConnect.concat(DEFAULT_PORT);
}
}
}
resetService();
this.serverHostNameOrIpAndPortDelimitedWithSemiColon = serverHostNameAndorPortToConnect;
this.serverHostNameOrIp = StringUtils.substringBefore(serverHostNameAndorPortToConnect, ":");
this.port = StringUtils.substringAfter(serverHostNameAndorPortToConnect, ":");
loadService();
}
private void loadService(){
boolean successfullyConnectedToLocalService = false;
successfullyConnectedToRemoteService = false;
if (clientWantsToConnectToInternalServer()){
successfullyConnectedToLocalService = loadService(true);
}
else{
//Client does not want to connect internal server
//Nevertheless it may be the case that 'localhost' or '127.0.0.1'
//has been specified. If so check first if an internal server
//is activated and if not then try a connection to a remote server
if (clientsWantsToConnectToLocalhost()){
successfullyConnectedToLocalService = loadService(true);
}
if (!successfullyConnectedToLocalService){
//Client wants to connect to a remote server
successfullyConnectedToRemoteService = loadService(false);
}
}
if (! successfullyConnectedToLocalService && ! successfullyConnectedToRemoteService ){
throw new CmsException("Unable to connect to local or remote server "+ serverHostNameOrIpAndPortDelimitedWithSemiColon);
}
logger.debug("Connected to server {} {}",serverHostNameOrIpAndPortDelimitedWithSemiColon,
(successfullyConnectedToLocalService? " locally ": " remotely"));
}
private boolean clientsWantsToConnectToLocalhost() {
return serverHostNameOrIpAndPortDelimitedWithSemiColon != null &&
( serverHostNameOrIpAndPortDelimitedWithSemiColon.toLowerCase().startsWith("localhost:") ||
serverHostNameOrIpAndPortDelimitedWithSemiColon.toLowerCase().startsWith("127.0.0.1:")
);
}
/*
* A Client is connected to Local Server if n server host port is provided
* or 'localhost' value is specified
*/
private boolean clientWantsToConnectToInternalServer(){
return serverHostNameOrIpAndPortDelimitedWithSemiColon == null ||
serverHostNameOrIpAndPortDelimitedWithSemiColon.toLowerCase().startsWith(AstroboaClient.INTERNAL_CONNECTION.toLowerCase()+":");
}
<R> Object connectToLocalService(Class<R> serviceClass){
return connectToLocalService(serviceClass, null);
}
<R> Object connectToLocalService(Class<R> serviceClass, String jndiName){
try {
if (StringUtils.isBlank(jndiName)){
jndiName = createJNDIBindingNameForService(serviceClass, true);
}
try{
return new InitialContext().lookup(jndiName);
}
catch (Exception e){
logger.warn("Could not connect to local service "+ serviceClass.getSimpleName(), e);
//try with '#' instead of '!'
if (jndiName.contains("!")){
return new InitialContext().lookup(jndiName.replace("!", "#"));
}
return null;
}
} catch (Exception e) {
logger.warn("",e);
return null;
}
}
<R> Object connectToRemoteService(Class<R> serviceClass){
return connectToRemoteService(serviceClass, null);
}
<R> Object connectToRemoteService(Class<R> serviceClass, String jndiName){
//Call this method to actually get the referenced object
try {
/*
* According to the documentation
* https://docs.jboss.org/author/display/AS71/EJB+invocations+from+a+remote+client+using+JNDI
*
* and to this thread https://community.jboss.org/message/647202#647202
*
* JBoss AS7 has changed the procedure for a remote client invocation.
* We have to use these JBoss Specific API in order to be able to dynamically provide information
* about the server host name or ip and the port.
*
* Bear in mind that in these properties no username and password is provided.
* This means that the JBoss AS7 which hosts the Astroboa must have its
* security-realm for the subsystem remoting disabled.
*
* This code must be reviewed as not only it contains JBoss Specific API but also
* remoting subsystem must be active without security
*/
Properties ejbClientConfigurationProperties = new Properties();
ejbClientConfigurationProperties.put("remote.connectionprovider.create.options.org.xnio.Options.SSL_ENABLED","false");
ejbClientConfigurationProperties.put("remote.connections", "default");
ejbClientConfigurationProperties.put("remote.connection.default.host", serverHostNameOrIp);
ejbClientConfigurationProperties.put("remote.connection.default.port", port);
ejbClientConfigurationProperties.put("remote.connection.default.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS", "false");
final EJBClientConfiguration ejbClientConfiguration = new PropertiesBasedEJBClientConfiguration(ejbClientConfigurationProperties);
// EJB client context selection is based on selectors. So let's create a ConfigBasedEJBClientContextSelector which uses our EJBClientConfiguration created in previous step
final ContextSelector<EJBClientContext> ejbClientContextSelector = new ConfigBasedEJBClientContextSelector(ejbClientConfiguration);
// Now let's setup the EJBClientContext to use this selector
final ContextSelector<EJBClientContext> previousSelector = EJBClientContext.setSelector(ejbClientContextSelector);
////////////////
//Context environmental properties specific to JBoss Naming Context
Properties jndiEnvironmentProperties = new Properties();
//jndiEnvironmentProperties.put(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory");
jndiEnvironmentProperties.put(Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming");
//jndiEnvironmentProperties.put(Context.PROVIDER_URL, "jnp://"+serverHostNameOrIpAndPortDelimitedWithSemiColon);
InitialContext context = new InitialContext(jndiEnvironmentProperties);
if (StringUtils.isBlank(jndiName)){
jndiName = createJNDIBindingNameForService(serviceClass, false);
}
try{
return context.lookup(jndiName);
}
catch (Exception e){
logger.warn("Could not connect to local service "+ serviceClass.getSimpleName(), e);
//try with '#' instead of '!'
if (jndiName.contains("!")){
return context.lookup(jndiName.replace("!", "#"));
}
return null;
}
} catch (Exception e) {
logger.error("",e);
return null;
}
}
public String getAuthenticationToken() {
return client.getAuthenticationToken();
}
public boolean isClientConnectedToARemoteServer()
{
return successfullyConnectedToRemoteService;
}
private <R> String createJNDIBindingNameForService(Class<R> serviceClass, boolean local){
/*
* In JBoss 5 and onwards all Astroboa EJB beans have been annotated using the following annotations
*
* @Local({<local-service-api-name>.class})
* @Remote({<remote-service-api-name>.class})
* @Stateless("<service-name>")
*
* where "<local-service-api-name>" and "<remote-service-api-name>" is the name of the local
* and remote interface respectively and "<service-name>" which is the simple name of the local interface
*
* For example, EJB for ContentServiceSecure is annotated as
*
* @Local(ContentServiceSecure.class)
* @Remote(RemoteContentServiceSecure.class)
* @Stateless("ContentServiceSecure")
*
* JBoss 5 EJB engine exposes Astroboa EJB beans under the Global JNDI namespace, using the following
* JNDI names
* <service-name>/local
* <service-name>/local-<fully-qualified-name-of-local-interface>
*
* <service-name>/remote
* <service-name>/remote-<fully-qualified-name-of-remote-interface>
*
* Therefore, for the above example the JNDI names of the local and remote ejb beans for ContentServiceSecure are
*
* ContentServiceSecure/local
* ContentServiceSecure/local-org.betaconceptframework.astroboa.api.service.secure.ContentServiceSecure
*
* ContentServiceSecure/remote
* ContentServiceSecure/remote-org.betaconceptframework.astroboa.api.service.secure.RemoteContentServiceSecure
*
* The JNDI view as displayed by JMX-Console for "ContentServiceSecure" EJB Bean is
*
* +- ContentServiceSecure (class: org.jnp.interfaces.NamingContext)
* | +- local (class: Proxy for: org.betaconceptframework.astroboa.api.service.secure.ContentServiceSecure)
* | +- remote-org.betaconceptframework.astroboa.api.service.secure.remote.RemoteContentServiceSecure (class: Proxy for: org.betaconceptframework.astroboa.api.service.secure.remote.RemoteContentServiceSecure)
* | +- local-org.betaconceptframework.astroboa.api.service.secure.ContentServiceSecure (class: Proxy for: org.betaconceptframework.astroboa.api.service.secure.ContentServiceSecure)
* | +- remote (class: Proxy for: org.betaconceptframework.astroboa.api.service.secure.remote.RemoteContentServiceSecure)
*
*
* So in oder to calculate the JNDI name for the provided service,
* we use the simple name of the class suffixed with "/local" or "remote"
*
*/
//return serviceClass.getSimpleName()+"/"+ (local?"local":"remote");
/*
* In EJB 3.1, the global JNDI names would follow a standard pattern that is portable across all the different application servers:
*
* java:global[/<application-name>]/<module-name>/<bean-name>#<interface-name>
*
* In JBoss AS 7 the JNDI bindings for session bean named TaxonomyServiceSecure in deployment unit subdeployment "astroboa-ejb3.jar" of deployment "astroboa.ear"
* are as follows:
*
* java:global/astroboa/astroboa-ejb3/TaxonomyServiceSecure!org.betaconceptframework.astroboa.api.service.secure.remote.RemoteTaxonomyServiceSecure
* java:app/astroboa-ejb3/TaxonomyServiceSecure!org.betaconceptframework.astroboa.api.service.secure.remote.RemoteTaxonomyServiceSecure
* java:module/TaxonomyServiceSecure!org.betaconceptframework.astroboa.api.service.secure.remote.RemoteTaxonomyServiceSecure
* java:jboss/exported/astroboa/astroboa-ejb3/TaxonomyServiceSecure!org.betaconceptframework.astroboa.api.service.secure.remote.RemoteTaxonomyServiceSecure
* java:global/astroboa/astroboa-ejb3/TaxonomyServiceSecure!org.betaconceptframework.astroboa.api.service.secure.TaxonomyServiceSecure
* java:app/astroboa-ejb3/TaxonomyServiceSecure!org.betaconceptframework.astroboa.api.service.secure.TaxonomyServiceSecure
* java:module/TaxonomyServiceSecure!org.betaconceptframework.astroboa.api.service.secure.TaxonomyServiceSecure
*
* Note the '!' instead of the '#' character in JBoss
*
* For remote JNDI connections see more info in
* https://docs.jboss.org/author/display/AS71/EJB+invocations+from+a+remote+client+using+JNDI
*/
if (local){
return "java:global/"+ASTROBOA_EAR_APPLICATION_NAME+"/"+ASTROBOA_EJB_MODULE_NAME+"/"+serviceClass.getSimpleName()+"!"+ serviceClass.getName();
}
else{
/*
* For remote JNDI connections see more info in
* https://docs.jboss.org/author/display/AS71/EJB+invocations+from+a+remote+client+using+JNDI
*
* Stateless beans
* ejb:<app-name>/<module-name>/<distinct-name>/<bean-name>!<fully-qualified-classname-of-the-remote-interface>
*
* Statefull beans
* ejb:<app-name>/<module-name>/<distinct-name>/<bean-name>!<fully-qualified-classname-of-the-remote-interface>?stateful
*
*
* app-name : This is the name of the .ear (without the .ear suffix) that you have deployed on the server and contains your EJBs.
* Java EE 6 allows you to override the application name, to a name of your choice by setting it in the application.xml.
* If the deployment uses uses such an override then the app-name used in the JNDI name should match that name.
* EJBs can also be deployed in a .war or a plain .jar. In such cases where the deployment isn't an .ear file,
* then the app-name must be an empty string, while doing the lookup.
* module-name : This is the name of the .jar (without the .jar suffix) that you have deployed on the server and the contains your EJBs.
* If the EJBs are deployed in a .war then the module name is the .war name (without the .war suffix). Java EE 6 allows you to
* override the module name, by setting it in the ejb-jar.xml/web.xml of your deployment. If the deployment uses such an override
* then the module-name used in the JNDI name should match that name. Module name part cannot be an empty string in the JNDI name.
* distinct-name : This is a JBoss AS7 specific name which can be optionally assigned to the deployments that are deployed on the server.
* More about the purpose and usage of this will be explained in a separate chapter. If a deployment doesn't use distinct-name then,
* use an empty string in the JNDI name, for distinct-name.
* bean-name : This is the name of the bean for which you are doing the lookup. The bean name is typically the unqualified classname
* of the bean implementation class, but can be overriden through either ejb-jar.xml or via annotations. The bean name part cannot
* be an empty string in the JNDI name.
* fully-qualified-classname-of-the-remote-interface : This is the fully qualified class name of the interface for which you are doing the lookup.
* The interface should be one of the remote interfaces exposed by the bean on the server. The fully qualified class name part cannot be
* an empty string in the JNDI name.
*
* All Astroboa EJB3 beans are stateless
*
*/
String distinctName = "";
return "ejb:"+ASTROBOA_EAR_APPLICATION_NAME+"/"+ASTROBOA_EJB_MODULE_NAME+"/"+distinctName+"/"+serviceClass.getSimpleName()+"!"+ serviceClass.getName();
}
}
}