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