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