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 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    }