1 /*
2  * Copyright (C) 2010 Google, Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.inject.persist.jpa;
18 
19 import com.google.common.base.Preconditions;
20 import com.google.common.collect.Lists;
21 import com.google.inject.Inject;
22 import com.google.inject.Provides;
23 import com.google.inject.Singleton;
24 import com.google.inject.persist.PersistModule;
25 import com.google.inject.persist.PersistService;
26 import com.google.inject.persist.UnitOfWork;
27 import com.google.inject.persist.finder.DynamicFinder;
28 import com.google.inject.persist.finder.Finder;
29 import java.lang.reflect.AccessibleObject;
30 import java.lang.reflect.InvocationHandler;
31 import java.lang.reflect.Method;
32 import java.lang.reflect.Proxy;
33 import java.util.List;
34 import java.util.Map;
35 import javax.persistence.EntityManager;
36 import javax.persistence.EntityManagerFactory;
37 import org.aopalliance.intercept.MethodInterceptor;
38 import org.aopalliance.intercept.MethodInvocation;
39 
40 /**
41  * JPA provider for guice persist.
42  *
43  * @author dhanji@gmail.com (Dhanji R. Prasanna)
44  */
45 public final class JpaPersistModule extends PersistModule {
46   private final String jpaUnit;
47 
JpaPersistModule(String jpaUnit)48   public JpaPersistModule(String jpaUnit) {
49     Preconditions.checkArgument(
50         null != jpaUnit && jpaUnit.length() > 0, "JPA unit name must be a non-empty string.");
51     this.jpaUnit = jpaUnit;
52   }
53 
54   private Map<?, ?> properties;
55   private MethodInterceptor transactionInterceptor;
56 
57   @Override
configurePersistence()58   protected void configurePersistence() {
59     bindConstant().annotatedWith(Jpa.class).to(jpaUnit);
60 
61     bind(JpaPersistService.class).in(Singleton.class);
62 
63     bind(PersistService.class).to(JpaPersistService.class);
64     bind(UnitOfWork.class).to(JpaPersistService.class);
65     bind(EntityManager.class).toProvider(JpaPersistService.class);
66     bind(EntityManagerFactory.class)
67         .toProvider(JpaPersistService.EntityManagerFactoryProvider.class);
68 
69     transactionInterceptor = new JpaLocalTxnInterceptor();
70     requestInjection(transactionInterceptor);
71 
72     // Bind dynamic finders.
73     for (Class<?> finder : dynamicFinders) {
74       bindFinder(finder);
75     }
76   }
77 
78   @Override
getTransactionInterceptor()79   protected MethodInterceptor getTransactionInterceptor() {
80     return transactionInterceptor;
81   }
82 
83   @Provides
84   @Jpa
provideProperties()85   Map<?, ?> provideProperties() {
86     return properties;
87   }
88 
89   /**
90    * Configures the JPA persistence provider with a set of properties.
91    *
92    * @param properties A set of name value pairs that configure a JPA persistence provider as per
93    *     the specification.
94    * @since 4.0 (since 3.0 with a parameter type of {@code java.util.Properties})
95    */
properties(Map<?, ?> properties)96   public JpaPersistModule properties(Map<?, ?> properties) {
97     this.properties = properties;
98     return this;
99   }
100 
101   private final List<Class<?>> dynamicFinders = Lists.newArrayList();
102 
103   /**
104    * Adds an interface to this module to use as a dynamic finder.
105    *
106    * @param iface Any interface type whose methods are all dynamic finders.
107    */
addFinder(Class<T> iface)108   public <T> JpaPersistModule addFinder(Class<T> iface) {
109     dynamicFinders.add(iface);
110     return this;
111   }
112 
bindFinder(Class<T> iface)113   private <T> void bindFinder(Class<T> iface) {
114     if (!isDynamicFinderValid(iface)) {
115       return;
116     }
117 
118     InvocationHandler finderInvoker =
119         new InvocationHandler() {
120           @Inject JpaFinderProxy finderProxy;
121 
122           @Override
123           public Object invoke(final Object thisObject, final Method method, final Object[] args)
124               throws Throwable {
125 
126             // Don't intercept non-finder methods like equals and hashcode.
127             if (!method.isAnnotationPresent(Finder.class)) {
128               // NOTE(dhanji): This is not ideal, we are using the invocation handler's equals
129               // and hashcode as a proxy (!) for the proxy's equals and hashcode.
130               return method.invoke(this, args);
131             }
132 
133             return finderProxy.invoke(
134                 new MethodInvocation() {
135                   @Override
136                   public Method getMethod() {
137                     return method;
138                   }
139 
140                   @Override
141                   public Object[] getArguments() {
142                     return null == args ? new Object[0] : args;
143                   }
144 
145                   @Override
146                   public Object proceed() throws Throwable {
147                     return method.invoke(thisObject, args);
148                   }
149 
150                   @Override
151                   public Object getThis() {
152                     throw new UnsupportedOperationException(
153                         "Bottomless proxies don't expose a this.");
154                   }
155 
156                   @Override
157                   public AccessibleObject getStaticPart() {
158                     throw new UnsupportedOperationException();
159                   }
160                 });
161           }
162         };
163     requestInjection(finderInvoker);
164 
165     @SuppressWarnings("unchecked") // Proxy must produce instance of type given.
166     T proxy =
167         (T)
168             Proxy.newProxyInstance(
169                 Thread.currentThread().getContextClassLoader(),
170                 new Class<?>[] {iface},
171                 finderInvoker);
172 
173     bind(iface).toInstance(proxy);
174   }
175 
isDynamicFinderValid(Class<?> iface)176   private boolean isDynamicFinderValid(Class<?> iface) {
177     boolean valid = true;
178     if (!iface.isInterface()) {
179       addError(iface + " is not an interface. Dynamic Finders must be interfaces.");
180       valid = false;
181     }
182 
183     for (Method method : iface.getMethods()) {
184       DynamicFinder finder = DynamicFinder.from(method);
185       if (null == finder) {
186         addError(
187             "Dynamic Finder methods must be annotated with @Finder, but "
188                 + iface
189                 + "."
190                 + method.getName()
191                 + " was not");
192         valid = false;
193       }
194     }
195     return valid;
196   }
197 }
198