1 package com
.google
.appengine
.api
.datastore
;
3 import static com
.google
.common
.base
.Preconditions
.checkArgument
;
4 import static com
.google
.common
.base
.Preconditions
.checkNotNull
;
6 import com
.google
.api
.client
.http
.HttpRequest
;
7 import com
.google
.api
.client
.http
.HttpRequestInitializer
;
8 import com
.google
.api
.services
.datastore
.DatastoreV1
;
9 import com
.google
.api
.services
.datastore
.client
.Datastore
;
10 import com
.google
.api
.services
.datastore
.client
.DatastoreException
;
11 import com
.google
.api
.services
.datastore
.client
.DatastoreFactory
;
12 import com
.google
.api
.services
.datastore
.client
.DatastoreHelper
;
13 import com
.google
.api
.services
.datastore
.client
.DatastoreOptions
;
14 import com
.google
.appengine
.api
.datastore
.DatastoreServiceConfig
.ApiVersion
;
15 import com
.google
.apphosting
.api
.ApiProxy
;
16 import com
.google
.apphosting
.api
.ApiProxy
.ApiConfig
;
17 import com
.google
.apphosting
.api
.ApiProxy
.ApiProxyException
;
18 import com
.google
.apphosting
.api
.ApiProxy
.Delegate
;
19 import com
.google
.apphosting
.api
.ApiProxy
.Environment
;
20 import com
.google
.apphosting
.api
.ApiProxy
.EnvironmentFactory
;
21 import com
.google
.apphosting
.api
.ApiProxy
.LogRecord
;
22 import com
.google
.apphosting
.datastore
.DatastoreV4
.AllocateIdsRequest
;
23 import com
.google
.apphosting
.datastore
.DatastoreV4
.AllocateIdsResponse
;
24 import com
.google
.apphosting
.datastore
.DatastoreV4
.BeginTransactionRequest
;
25 import com
.google
.apphosting
.datastore
.DatastoreV4
.BeginTransactionResponse
;
26 import com
.google
.apphosting
.datastore
.DatastoreV4
.CommitRequest
;
27 import com
.google
.apphosting
.datastore
.DatastoreV4
.CommitResponse
;
28 import com
.google
.apphosting
.datastore
.DatastoreV4
.ContinueQueryRequest
;
29 import com
.google
.apphosting
.datastore
.DatastoreV4
.ContinueQueryResponse
;
30 import com
.google
.apphosting
.datastore
.DatastoreV4
.LookupRequest
;
31 import com
.google
.apphosting
.datastore
.DatastoreV4
.LookupResponse
;
32 import com
.google
.apphosting
.datastore
.DatastoreV4
.RollbackRequest
;
33 import com
.google
.apphosting
.datastore
.DatastoreV4
.RollbackResponse
;
34 import com
.google
.apphosting
.datastore
.DatastoreV4
.RunQueryRequest
;
35 import com
.google
.apphosting
.datastore
.DatastoreV4
.RunQueryResponse
;
36 import com
.google
.protobuf
.Message
;
38 import java
.io
.IOException
;
39 import java
.security
.GeneralSecurityException
;
40 import java
.util
.ConcurrentModificationException
;
41 import java
.util
.HashMap
;
42 import java
.util
.List
;
44 import java
.util
.concurrent
.Callable
;
45 import java
.util
.concurrent
.ExecutorService
;
46 import java
.util
.concurrent
.Executors
;
47 import java
.util
.concurrent
.Future
;
48 import java
.util
.regex
.Matcher
;
49 import java
.util
.regex
.Pattern
;
51 import javax
.servlet
.http
.HttpServletResponse
;
54 * Redirects API calls to Google Cloud Datastore.
56 final class CloudDatastoreProxy
implements DatastoreV4Proxy
{
58 private static final ExecutorService executor
= Executors
.newCachedThreadPool();
60 private static final CloudDatastoreProtoConverter CONVERTER
=
61 CloudDatastoreProtoConverter
.getInstance();
63 private final Datastore datastore
;
65 CloudDatastoreProxy(Datastore datastore
) {
66 this.datastore
= checkNotNull(datastore
);
70 * Creates a {@link CloudDatastoreProxy}. This method has the side effect
71 * of installing minimal stubs ({@link EnvironmentFactory} and
72 * {@link Delegate}) in the API proxy if they have not already been installed.
74 static CloudDatastoreProxy
create(DatastoreServiceConfig config
) {
75 checkArgument(config
.getApiVersion() == ApiVersion
.CLOUD_DATASTORE
);
76 DatastoreOptions options
= getDatastoreOptions(config
);
77 ensureApiProxyIsConfigured(options
);
78 return new CloudDatastoreProxy(DatastoreFactory
.get().create(options
));
82 public Future
<BeginTransactionResponse
> beginTransaction(BeginTransactionRequest v4Request
) {
83 return makeCall(new Callable
<BeginTransactionResponse
>() {
85 public BeginTransactionResponse
call() throws DatastoreException
{
86 return CONVERTER
.toV4BeginTransactionResponse(datastore
.beginTransaction(
87 DatastoreV1
.BeginTransactionRequest
.getDefaultInstance())).build();
93 public Future
<RollbackResponse
> rollback(final RollbackRequest v4Request
) {
94 return makeCall(new Callable
<RollbackResponse
>() {
96 public RollbackResponse
call() throws DatastoreException
{
97 datastore
.rollback(CONVERTER
.toV1RollbackRequest(v4Request
).build());
98 return RollbackResponse
.getDefaultInstance();
104 public Future
<RunQueryResponse
> runQuery(final RunQueryRequest v4Request
) {
105 return makeCall(new Callable
<RunQueryResponse
>() {
107 public RunQueryResponse
call() throws DatastoreException
{
108 return CONVERTER
.toV4RunQueryResponse(datastore
.runQuery(
109 CONVERTER
.toV1RunQueryRequest(v4Request
).build())).build();
115 public Future
<ContinueQueryResponse
> continueQuery(ContinueQueryRequest v4Request
) {
116 throw new UnsupportedOperationException();
120 public Future
<LookupResponse
> lookup(final LookupRequest v4Request
) {
121 return makeCall(new Callable
<LookupResponse
>() {
123 public LookupResponse
call() throws DatastoreException
{
124 return CONVERTER
.toV4LookupResponse(datastore
.lookup(
125 CONVERTER
.toV1LookupRequest(v4Request
).build())).build();
131 public Future
<AllocateIdsResponse
> allocateIds(final AllocateIdsRequest v4Request
) {
132 return makeCall(new Callable
<AllocateIdsResponse
>() {
134 public AllocateIdsResponse
call() throws DatastoreException
{
135 return CONVERTER
.toV4AllocateIdsResponse(datastore
.allocateIds(
136 CONVERTER
.toV1AllocateIdsRequest(v4Request
).build())).build();
142 public Future
<CommitResponse
> commit(final CommitRequest v4Request
) {
143 return makeCall(new Callable
<CommitResponse
>() {
145 public CommitResponse
call() throws DatastoreException
{
146 return CONVERTER
.toV4CommitResponse(datastore
.commit(
147 CONVERTER
.toV1CommitRequest(v4Request
).build()), v4Request
).build();
153 public Future
<CommitResponse
> rawCommit(byte[] v4Request
) {
154 throw new UnsupportedOperationException();
157 private static <T
extends Message
> Future
<T
> makeCall(final Callable
<T
> request
) {
158 return executor
.submit(new Callable
<T
>() {
160 public T
call() throws Exception
{
162 return request
.call();
163 } catch (DatastoreException e
) {
164 throw extractException(e
.getMessage(), e
.getCode());
170 private static final Pattern reasonPattern
= Pattern
.compile("\"reason\": \"(.*)\",?\\n");
171 private static final Pattern messagePattern
= Pattern
.compile("\"message\": \"(.*)\",?\\n");
174 * Convert the exception thrown by Cloud Datastore to version comparable
175 * to the Exceptions thrown by ApiProxy.
177 protected static Exception
extractException(String rawMessage
, int httpCode
) {
178 Matcher msgMatcher
= messagePattern
.matcher(rawMessage
);
179 String message
= msgMatcher
.find() ? msgMatcher
.group(1) : "[" + rawMessage
+ "]\n";
181 case HttpServletResponse
.SC_BAD_REQUEST
:
182 return new IllegalArgumentException(message
);
183 case HttpServletResponse
.SC_FORBIDDEN
:
184 Matcher reasonMatch
= reasonPattern
.matcher(rawMessage
);
185 if (reasonMatch
.find() && reasonMatch
.group(1).equals("DEADLINE_EXCEEDED")) {
186 return new DatastoreTimeoutException(message
);
188 return new IllegalStateException(message
);
190 case HttpServletResponse
.SC_PRECONDITION_FAILED
:
191 return new DatastoreNeedIndexException(message
);
192 case HttpServletResponse
.SC_CONFLICT
:
193 return new ConcurrentModificationException(message
);
194 case HttpServletResponse
.SC_SERVICE_UNAVAILABLE
:
195 return new IllegalStateException(message
);
196 case HttpServletResponse
.SC_INTERNAL_SERVER_ERROR
:
197 return new DatastoreFailureException(message
);
198 case HttpServletResponse
.SC_PAYMENT_REQUIRED
:
200 return new RuntimeException(message
);
204 private static DatastoreOptions
getDatastoreOptions(DatastoreServiceConfig config
) {
205 if (config
.getDatasetForTest() != null) {
206 return getOptionsFromConfig(config
);
208 return getOptionsFromEnv();
212 private static DatastoreOptions
getOptionsFromConfig(final DatastoreServiceConfig config
) {
213 DatastoreOptions
.Builder optionsBuilder
= new DatastoreOptions
.Builder()
214 .dataset(config
.getDatasetForTest());
215 if (config
.getHost() != null) {
216 optionsBuilder
.host(config
.getHost());
218 if (config
.getVersionOverride() != null) {
219 optionsBuilder
.initializer(new HttpRequestInitializer() {
221 public void initialize(HttpRequest request
) throws IOException
{
222 request
.getUrl().setRawPath(
223 request
.getUrl().getRawPath().replaceFirst(
224 DatastoreFactory
.VERSION
, config
.getVersionOverride()));
228 if (config
.getServiceAccountForTest() != null && config
.getPrivateKeyFileForTest() != null) {
230 optionsBuilder
.credential(DatastoreHelper
.getServiceAccountCredential(
231 config
.getServiceAccountForTest(), config
.getPrivateKeyFileForTest()));
232 } catch (IOException
| GeneralSecurityException e
) {
233 throw new RuntimeException(
234 "Could not get Cloud Datastore options from DatastoreServiceConfig.", e
);
238 return optionsBuilder
.build();
241 private static DatastoreOptions
getOptionsFromEnv() {
243 return DatastoreHelper
.getOptionsfromEnv().build();
244 } catch (IOException
| GeneralSecurityException e
) {
245 throw new RuntimeException(
246 "Could not get Cloud Datastore from environment.", e
);
251 * Make sure that the API proxy has been configured. If it's already
252 * configured (e.g. because the Remote API has been installed or the factory
253 * has already been used), do nothing. Otherwise, install a stub environment
256 private static synchronized void ensureApiProxyIsConfigured(DatastoreOptions options
) {
257 boolean hasEnvironmentOrFactory
= (ApiProxy
.getCurrentEnvironment() != null);
258 boolean hasDelegate
= (ApiProxy
.getDelegate() != null);
260 if (hasEnvironmentOrFactory
&& hasDelegate
) {
264 if (hasEnvironmentOrFactory
) {
265 throw new IllegalStateException(
266 "An ApiProxy.Environment or ApiProxy.EnvironmentFactory was already installed. "
267 + "Cannot use Cloud Datastore.");
268 } else if (hasDelegate
) {
269 throw new IllegalStateException(
270 "An ApiProxy.Delegate was already installed. Cannot use Cloud Datastore.");
273 ApiProxy
.setEnvironmentFactory(
274 new StubApiProxyEnvironmentFactory(getFullAppId(options
)));
275 ApiProxy
.setDelegate(new StubApiProxyDelegate());
279 * Attempt to determine the full app id. This is only necessary if the client
280 * did not install the Remote API (which will determine it automatically).
282 * By default, take the dataset from the provided {@link DatastoreOptions} and
283 * prepend {@code s~}. Apps for which this is incorrect (e.g. apps running in
284 * Europe) can specify the full app id via the {@code _DATASTORE_FULL_DATASET}
285 * environment variable.
287 private static String
getFullAppId(DatastoreOptions options
) {
288 String fullDataset
= EnvProxy
.getenv("_DATASTORE_FULL_DATASET");
289 if (fullDataset
!= null) {
292 return "s~" + options
.getDataset();
296 * A {@link Delegate} that throws {@link UnsupportedOperationException} for
299 static class StubApiProxyDelegate
implements Delegate
<Environment
> {
300 private static final String UNSUPPORTED_API_PATTERN
=
301 "Calls to %s.%s are not supported under this configuration, only "
302 + "calls to Cloud Datastore. To use other APIs, first install the "
306 public byte[] makeSyncCall(Environment environment
, String packageName
,
307 String methodName
, byte[] request
) throws ApiProxyException
{
308 throw new UnsupportedOperationException(
309 String
.format(UNSUPPORTED_API_PATTERN
, packageName
, methodName
));
313 public Future
<byte[]> makeAsyncCall(Environment environment
, String packageName
,
314 String methodName
, byte[] request
, ApiConfig apiConfig
) {
315 throw new UnsupportedOperationException(
316 String
.format(UNSUPPORTED_API_PATTERN
, packageName
, methodName
));
320 public void log(Environment environment
, LogRecord record
) {
321 throw new UnsupportedOperationException();
325 public void flushLogs(Environment environment
) {
326 throw new UnsupportedOperationException();
330 public List
<Thread
> getRequestThreads(Environment environment
) {
331 throw new UnsupportedOperationException();
336 * An {@link EnvironmentFactory} that builds {@link StubApiProxyEnvironment}s.
338 static class StubApiProxyEnvironmentFactory
implements EnvironmentFactory
{
339 private final String appId
;
341 public StubApiProxyEnvironmentFactory(String appId
) {
346 public Environment
newEnvironment() {
347 return new StubApiProxyEnvironment(appId
);
352 * An {@link Environment} that supports the minimal subset of features needed
353 * to run code from the datastore package outside of App Engine. All other
354 * methods throw {@link UnsupportedOperationException}.
356 static class StubApiProxyEnvironment
implements Environment
{
357 private final Map
<String
, Object
> attributes
;
358 private final String appId
;
360 public StubApiProxyEnvironment(String appId
) {
361 this.attributes
= new HashMap
<>();
366 public boolean isLoggedIn() {
367 throw new UnsupportedOperationException();
371 public boolean isAdmin() {
372 throw new UnsupportedOperationException();
376 public String
getVersionId() {
377 throw new UnsupportedOperationException();
382 public String
getRequestNamespace() {
383 throw new UnsupportedOperationException();
387 public long getRemainingMillis() {
388 throw new UnsupportedOperationException();
392 public String
getModuleId() {
393 throw new UnsupportedOperationException();
397 public String
getEmail() {
398 throw new UnsupportedOperationException();
402 public String
getAuthDomain() {
403 throw new UnsupportedOperationException();
407 public Map
<String
, Object
> getAttributes() {
412 public String
getAppId() {