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 */
022package org.granite.tide.ejb;
023
024import java.io.IOException;
025import java.io.ObjectInputStream;
026import java.io.ObjectOutputStream;
027import java.lang.reflect.Method;
028import java.util.ArrayList;
029import java.util.HashSet;
030import java.util.List;
031import java.util.Map;
032import java.util.Set;
033import java.util.concurrent.ConcurrentHashMap;
034
035import javax.ejb.NoSuchEJBException;
036import javax.naming.InitialContext;
037import javax.naming.NamingException;
038import javax.persistence.EntityManager;
039import javax.persistence.EntityManagerFactory;
040
041import org.granite.logging.Logger;
042import org.granite.messaging.service.EjbServiceMetadata;
043import org.granite.messaging.service.ServiceException;
044import org.granite.messaging.service.ServiceInvocationContext;
045import org.granite.tide.IInvocationCall;
046import org.granite.tide.IInvocationResult;
047import org.granite.tide.TidePersistenceManager;
048import org.granite.tide.TideServiceContext;
049import org.granite.tide.annotations.BypassTideMerge;
050import org.granite.tide.async.AsyncPublisher;
051import org.granite.tide.data.DataContext;
052import org.granite.tide.data.JPAPersistenceManager;
053import org.granite.tide.invocation.ContextEvent;
054import org.granite.tide.invocation.ContextUpdate;
055import org.granite.tide.invocation.InvocationCall;
056import org.granite.tide.invocation.InvocationResult;
057import org.granite.tide.util.AbstractContext;
058
059
060/**
061 * @author William DRAI
062 */
063public 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}