001/*
002  GRANITE DATA SERVICES
003  Copyright (C) 2011 GRANITE DATA SERVICES S.A.S.
004
005  This file is part of Granite Data Services.
006
007  Granite Data Services is free software; you can redistribute it and/or modify
008  it under the terms of the GNU Library General Public License as published by
009  the Free Software Foundation; either version 2 of the License, or (at your
010  option) any later version.
011
012  Granite Data Services is distributed in the hope that it will be useful, but
013  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
014  FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
015  for more details.
016
017  You should have received a copy of the GNU Library General Public License
018  along with this library; if not, see <http://www.gnu.org/licenses/>.
019*/
020
021package org.granite.tide.ejb;
022
023import java.io.IOException;
024import java.io.ObjectInputStream;
025import java.io.ObjectOutputStream;
026import java.lang.reflect.Method;
027import java.util.ArrayList;
028import java.util.HashSet;
029import java.util.List;
030import java.util.Map;
031import java.util.Set;
032import java.util.concurrent.ConcurrentHashMap;
033
034import javax.ejb.NoSuchEJBException;
035import javax.naming.InitialContext;
036import javax.naming.NamingException;
037import javax.persistence.EntityManager;
038import javax.persistence.EntityManagerFactory;
039
040import org.granite.logging.Logger;
041import org.granite.messaging.service.EjbServiceMetadata;
042import org.granite.messaging.service.ServiceException;
043import org.granite.messaging.service.ServiceInvocationContext;
044import org.granite.tide.IInvocationCall;
045import org.granite.tide.IInvocationResult;
046import org.granite.tide.TidePersistenceManager;
047import org.granite.tide.TideServiceContext;
048import org.granite.tide.annotations.BypassTideMerge;
049import org.granite.tide.async.AsyncPublisher;
050import org.granite.tide.data.DataContext;
051import org.granite.tide.data.JPAPersistenceManager;
052import org.granite.tide.invocation.ContextEvent;
053import org.granite.tide.invocation.ContextUpdate;
054import org.granite.tide.invocation.InvocationCall;
055import org.granite.tide.invocation.InvocationResult;
056import org.granite.tide.util.AbstractContext;
057
058
059/**
060 * @author William DRAI
061 */
062public class EjbServiceContext extends TideServiceContext  {
063
064    private static final long serialVersionUID = 1L;
065    
066    private static final Logger log = Logger.getLogger(EjbServiceContext.class);
067    
068    public static final String CAPITALIZED_DESTINATION_ID = "{capitalized.component.name}";
069    public static final String DESTINATION_ID = "{component.name}";
070    
071    private transient ConcurrentHashMap<String, EjbComponent> ejbLookupCache = new ConcurrentHashMap<String, EjbComponent>();
072    private final Set<String> remoteObservers = new HashSet<String>();
073    
074    private final InitialContext initialContext;
075    private final String lookup;
076    
077    private final EjbIdentity identity;
078    
079    private String entityManagerFactoryJndiName = null;
080    private String entityManagerJndiName = null;
081    
082    
083    public EjbServiceContext() throws ServiceException {
084        super();
085        lookup = "";
086        initialContext = null;
087        identity = new EjbIdentity();
088    }
089    
090    public EjbServiceContext(String lookup, InitialContext ic) throws ServiceException {
091        super();
092        this.lookup = lookup;
093        this.initialContext = ic;
094        identity = new EjbIdentity();
095    }
096
097
098    @Override
099    protected AsyncPublisher getAsyncPublisher() {
100        return null;
101    }
102    
103    
104    public void setEntityManagerFactoryJndiName(String entityManagerFactoryJndiName) {
105        this.entityManagerFactoryJndiName = entityManagerFactoryJndiName;
106    }
107
108    public void setEntityManagerJndiName(String entityManagerJndiName) {
109        this.entityManagerJndiName = entityManagerJndiName;
110    }
111    
112    /**
113     *  Create a TidePersistenceManager
114     *  
115     *  @param create create if not existent (can be false for use in entity merge)
116     *  @return a TidePersistenceManager
117     */
118    @Override
119        protected TidePersistenceManager getTidePersistenceManager(boolean create) {
120        if (!create)
121            return null;
122        
123        EntityManager em = getEntityManager();
124        if (em == null)
125            return null;
126        
127        return new JPAPersistenceManager(em);
128    }
129    
130    
131    /**
132     * Find the entity manager using the jndi names stored in the bean. 
133     * @return The found entity manager
134     */
135    private EntityManager getEntityManager() {
136        try {
137            InitialContext jndiContext = initialContext != null ? initialContext : new InitialContext();
138            
139            if (entityManagerFactoryJndiName != null) {
140                EntityManagerFactory factory = (EntityManagerFactory) jndiContext.lookup(entityManagerFactoryJndiName);
141                return factory.createEntityManager();
142            } 
143            else if (entityManagerJndiName != null) {
144                return (EntityManager) jndiContext.lookup(entityManagerJndiName);
145            }
146        } 
147        catch (NamingException e) {
148            if (entityManagerFactoryJndiName != null) 
149                throw new RuntimeException("Unable to find a EntityManagerFactory  for jndiName " + entityManagerFactoryJndiName);
150            else if (entityManagerJndiName != null) 
151                throw new RuntimeException("Unable to find a EntityManager for jndiName " + entityManagerJndiName);
152        }
153        
154        return null;
155    }
156
157    
158    public Object callComponent(Method method, Object... args) throws Exception {
159                String name = method.getDeclaringClass().getSimpleName();
160                name = name.substring(0, 1).toLowerCase() + name.substring(1);
161                if (name.endsWith("Bean"))
162                        name = name.substring(0, name.length() - "Bean".length());
163                Object invokee = findComponent(name, null);
164                method = invokee.getClass().getMethod(method.getName(), method.getParameterTypes());
165                return method.invoke(invokee, args);
166    }
167    
168    public Set<String> getRemoteObservers() {
169        return remoteObservers;
170    }
171    
172    /* (non-Javadoc)
173         * @see org.granite.tide.ejb.EJBServiceContextIntf#findComponent(java.lang.String)
174         */
175    @Override
176    public Object findComponent(String componentName, Class<?> componentClass) {
177        if ("identity".equals(componentName))
178                return identity;
179        
180        EjbComponent component = ejbLookupCache.get(componentName);
181        if (component != null)
182            return component.ejbInstance;
183        
184        // Compute EJB JNDI binding.
185        String name = componentName;
186        if (lookup != null) {
187            name = lookup;
188            if (lookup.contains(CAPITALIZED_DESTINATION_ID))
189                name = lookup.replace(CAPITALIZED_DESTINATION_ID, capitalize(componentName));
190            if (lookup.contains(DESTINATION_ID))
191                name = lookup.replace(DESTINATION_ID, componentName);
192        }
193        
194        InitialContext ic = this.initialContext;
195        if (ic == null) {
196                try {
197                    ic = new InitialContext();
198                } 
199                catch (Exception e) {
200                    throw new ServiceException("Could not get InitialContext", e);
201                }
202        }
203        
204        log.debug(">> New EjbServiceInvoker looking up: %s", name);
205
206        try {
207            component = new EjbComponent();
208            component.ejbInstance = ic.lookup(name);
209            component.ejbClasses = new HashSet<Class<?>>();
210            Class<?> scannedClass = null;
211            EjbScannedItemHandler itemHandler = EjbScannedItemHandler.instance();
212            for (Class<?> i : component.ejbInstance.getClass().getInterfaces()) {
213                if (itemHandler.getScannedClasses().containsKey(i)) {
214                        scannedClass = itemHandler.getScannedClasses().get(i);
215                        break;
216                }
217            }
218            if (scannedClass == null)
219                scannedClass = itemHandler.getScannedClasses().get(component.ejbInstance.getClass());
220            // GDS-768: handling of proxied no-interface EJBs in GlassFish v3
221            if (scannedClass == null && component.ejbInstance.getClass().getSuperclass() != null)
222                scannedClass = itemHandler.getScannedClasses().get(component.ejbInstance.getClass().getSuperclass());
223            
224            if (scannedClass != null) {
225                component.ejbClasses.add(scannedClass);
226                for (Map.Entry<Class<?>, Class<?>> me : itemHandler.getScannedClasses().entrySet()) {
227                        if (me.getValue().equals(scannedClass))
228                                component.ejbClasses.add(me.getKey());
229                }
230                component.ejbMetadata = new EjbServiceMetadata(scannedClass, component.ejbInstance.getClass());
231            }
232            else
233                log.warn("Ejb " + componentName + " was not scanned: remove method will not be called if it is a Stateful bean. Add META-INF/services-config.properties if needed.");
234            
235            EjbComponent tmpComponent = ejbLookupCache.putIfAbsent(componentName, component); 
236            if (tmpComponent != null) 
237                component = tmpComponent; 
238            return component.ejbInstance;
239        }
240        catch (NamingException e) {
241                log.error("EJB not found " + name + ": " + e.getMessage());
242            throw new ServiceException("Could not lookup for: " + name, e);
243        }
244    }
245    
246    /* (non-Javadoc)
247         * @see org.granite.tide.ejb.EJBServiceContextIntf#findComponentClass(java.lang.String)
248         */
249    @Override
250    public Set<Class<?>> findComponentClasses(String componentName, Class<?> componentClass) {
251        if ("identity".equals(componentName)) {
252                Set<Class<?>> classes = new HashSet<Class<?>>(1);
253                classes.add(EjbIdentity.class);
254                return classes;
255        }
256        
257        EjbComponent component = ejbLookupCache.get(componentName);
258        if (component == null)
259            findComponent(componentName, componentClass);
260        return ejbLookupCache.get(componentName).ejbClasses;
261    }
262
263    
264    private String capitalize(String s) {
265        if (s == null || s.length() == 0)
266            return s;
267        if (s.length() == 1)
268            return s.toUpperCase();
269        return s.substring(0, 1).toUpperCase() + s.substring(1);
270    }
271    
272    /* (non-Javadoc)
273         * @see org.granite.tide.ejb.EJBServiceContextIntf#prepareCall(org.granite.messaging.service.ServiceInvocationContext, org.granite.tide.IInvocationCall, java.lang.String)
274         */
275    @Override
276    public void prepareCall(ServiceInvocationContext context, IInvocationCall c, String componentName, Class<?> componentClass) {
277        if ((c instanceof InvocationCall) && ((InvocationCall)c).getListeners() != null)
278                remoteObservers.addAll(((InvocationCall)c).getListeners());
279        Context.create(this);
280                
281        // Initialize an empty data context
282        DataContext.init();
283    }
284
285    
286    private static class EjbComponent {
287        public Object ejbInstance;
288        public Set<Class<?>> ejbClasses;
289        public EjbServiceMetadata ejbMetadata;
290    }
291    
292    /* (non-Javadoc)
293         * @see org.granite.tide.ejb.EJBServiceContextIntf#postCall(org.granite.messaging.service.ServiceInvocationContext, java.lang.Object, java.lang.String)
294         */
295    @Override
296    public IInvocationResult postCall(ServiceInvocationContext context, Object result, String componentName, Class<?> componentClass) {
297        try {                   
298                AbstractContext threadContext = AbstractContext.instance();
299                
300                List<ContextUpdate> results = new ArrayList<ContextUpdate>(threadContext.size());
301                DataContext dataContext = DataContext.get();
302                Object[][] updates = dataContext != null ? dataContext.getUpdates() : null;
303                
304                for (Map.Entry<String, Object> entry : threadContext.entrySet())
305                        results.add(new ContextUpdate(entry.getKey(), null, entry.getValue(), 3, false));
306                
307                InvocationResult ires = new InvocationResult(result, results);
308                if (componentName != null || componentClass != null) {
309                                Set<Class<?>> componentClasses = findComponentClasses(componentName, componentClass);
310                        if (isBeanAnnotationPresent(componentClasses, context.getMethod().getName(), context.getMethod().getParameterTypes(), BypassTideMerge.class))
311                                        ires.setMerge(false);
312                }
313                
314                ires.setUpdates(updates);
315                ires.setEvents(new ArrayList<ContextEvent>(threadContext.getRemoteEvents()));
316                
317                if (componentName != null) {
318                    EjbComponent component = ejbLookupCache.get(componentName);
319                    if (component != null && component.ejbMetadata != null 
320                                && component.ejbMetadata.isStateful() && component.ejbMetadata.isRemoveMethod(context.getMethod()))
321                        ejbLookupCache.remove(componentName);
322                }
323                
324                return ires;
325        }
326        finally {
327                AbstractContext.remove();
328        }
329    }
330
331    /* (non-Javadoc)
332         * @see org.granite.tide.ejb.EJBServiceContextIntf#postCallFault(org.granite.messaging.service.ServiceInvocationContext, java.lang.Throwable, java.lang.String)
333         */
334    @Override
335    public void postCallFault(ServiceInvocationContext context, Throwable t, String componentName, Class<?> componentClass) {
336        try {
337                if (componentName != null) {
338                    EjbComponent component = ejbLookupCache.get(componentName);
339                    if (t instanceof NoSuchEJBException || (component != null && component.ejbMetadata != null &&
340                            (component.ejbMetadata.isStateful() &&
341                            component.ejbMetadata.isRemoveMethod(context.getMethod()) &&
342                            !component.ejbMetadata.getRetainIfException(context.getMethod()))
343                        )) {
344                        ejbLookupCache.remove(componentName);
345                    }
346                }
347        }
348        finally {
349                AbstractContext.remove();
350        }
351    }
352    
353    private void writeObject(ObjectOutputStream out) throws IOException {
354        out.defaultWriteObject(); 
355    }
356    
357    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
358        in.defaultReadObject();
359        ejbLookupCache = new ConcurrentHashMap<String, EjbComponent>();
360    }
361}