1 // Copyright 2009 Google Inc. All Rights Reserved.
3 package com
.google
.appengine
.tools
.development
;
5 import com
.google
.appengine
.api
.NamespaceManager
;
6 import com
.google
.apphosting
.api
.ApiProxy
;
7 import com
.google
.apphosting
.utils
.config
.WebModule
;
8 import com
.google
.common
.base
.Charsets
;
10 import java
.math
.BigInteger
;
11 import java
.nio
.ByteBuffer
;
12 import java
.security
.MessageDigest
;
13 import java
.util
.Collection
;
14 import java
.util
.Collections
;
15 import java
.util
.Date
;
17 import java
.util
.concurrent
.ConcurrentHashMap
;
18 import java
.util
.concurrent
.ConcurrentMap
;
19 import java
.util
.concurrent
.atomic
.AtomicInteger
;
20 import java
.util
.logging
.Level
;
21 import java
.util
.logging
.Logger
;
22 import java
.util
.regex
.Matcher
;
23 import java
.util
.regex
.Pattern
;
26 * {@code LocalEnvironment} is a simple
27 * {@link com.google.apphosting.api.ApiProxy.Environment} that reads
28 * application-specific details (e.g. application identifer) directly from the
29 * custom deployment descriptor.
32 abstract public class LocalEnvironment
implements ApiProxy
.Environment
{
33 private static final Logger logger
= Logger
.getLogger(LocalEnvironment
.class.getName());
34 static final Pattern APP_ID_PATTERN
= Pattern
.compile("([^:.]*)(:([^:.]*))?(.*)?");
36 private static final String APPS_NAMESPACE_KEY
=
37 NamespaceManager
.class.getName() + ".appsNamespace";
39 public static final Integer TESTING_DEFAULT_PORT
= new Integer(8080);
41 public static final String INSTANCE_ID_ENV_ATTRIBUTE
= "com.google.appengine.instance.id";
43 public static final String PORT_ID_ENV_ATTRIBUTE
= "com.google.appengine.instance.port";
46 * {@code ApiProxy.Environment} instances used with {@code
47 * ApiProxyLocalFactory} should have a entry in the map returned by
48 * {@code getAttributes()} with this key, and a {@link
49 * java.util.concurrent.Semaphore} as the value. This is used
50 * internally to track asynchronous API calls.
52 public static final String API_CALL_SEMAPHORE
=
53 "com.google.appengine.tools.development.api_call_semaphore";
56 * The name of an {@link #getAttributes() attribute} that contains a
57 * (String) unique identifier for the curent request.
59 public static final String REQUEST_ID
=
60 "com.google.appengine.runtime.request_log_id";
63 * The name of an {@link #getAttributes() attribute} that contains a
64 * a {@link Date} object representing the time this request was
67 public static final String START_TIME_ATTR
=
68 "com.google.appengine.tools.development.start_time";
71 * The name of an {@link #getAttributes() attribute} that contains a {@code
72 * Set<RequestEndListener>}. The set of {@link RequestEndListener
73 * RequestEndListeners} is populated by from within the service calls. The
74 * listeners are invoked at the end of a user request.
76 public static final String REQUEST_END_LISTENERS
=
77 "com.google.appengine.tools.development.request_end_listeners";
80 * The name of an {@link #getAttributes() attribute} that contains the {@link
81 * javax.servlet.http.HttpServletRequest} instance.
83 public static final String HTTP_SERVLET_REQUEST
=
84 "com.google.appengine.http_servlet_request";
86 private static final String REQUEST_THREAD_FACTORY_ATTR
=
87 "com.google.appengine.api.ThreadManager.REQUEST_THREAD_FACTORY";
89 private static final String BACKGROUND_THREAD_FACTORY_ATTR
=
90 "com.google.appengine.api.ThreadManager.BACKGROUND_THREAD_FACTORY";
93 * For historical and probably compatibility reasons dev appserver assigns all
94 * versions a minor version of 1.
96 private static final String MINOR_VERSION_SUFFIX
= ".1";
99 * In production, this is a constant that defines the {@link #getAttributes() attribute} name
100 * that contains the hostname on which the default version is listening.
101 * In the local development server, the {@link #getAttributes() attribute} contains the
102 * listening port in addition to the hostname, and is the one and only frontend app instance
105 public static final String DEFAULT_VERSION_HOSTNAME
=
106 "com.google.appengine.runtime.default_version_hostname";
108 private final String appId
;
109 private final String versionId
;
111 private final Collection
<RequestEndListener
> requestEndListeners
;
113 protected final ConcurrentMap
<String
, Object
> attributes
=
114 new ConcurrentHashMap
<String
, Object
>();
116 private final Long endTime
;
118 * Instance number for a main instance.
120 * Clients depend on this literal having the value -1 so do not change this
121 * value without making needed updates to clients.
123 public static final int MAIN_INSTANCE
= -1;
126 * Deprecated constructor used by tests.
129 protected LocalEnvironment(String appId
, String majorVersionId
) {
130 this(appId
, WebModule
.DEFAULT_MODULE_NAME
, majorVersionId
, LocalEnvironment
.MAIN_INSTANCE
,
131 TESTING_DEFAULT_PORT
, null);
135 * Deprecated constructor used by tests.
138 protected LocalEnvironment(String appId
, String majorVersionId
, Long deadlineMillis
) {
139 this(appId
, WebModule
.DEFAULT_MODULE_NAME
, majorVersionId
, LocalEnvironment
.MAIN_INSTANCE
,
140 TESTING_DEFAULT_PORT
, deadlineMillis
);
143 protected LocalEnvironment(String appId
, String moduleName
, String majorVersionId
, int instance
,
144 Integer port
, Long deadlineMillis
) {
146 if (moduleName
== null || WebModule
.DEFAULT_MODULE_NAME
.equals(moduleName
)) {
147 this.versionId
= majorVersionId
+ MINOR_VERSION_SUFFIX
;
149 this.versionId
= moduleName
+ ":" + majorVersionId
+ MINOR_VERSION_SUFFIX
;
151 if (deadlineMillis
== null) {
153 } else if (deadlineMillis
< 0) {
154 throw new IllegalArgumentException("deadlineMillis must be a non-negative integer.");
156 this.endTime
= System
.currentTimeMillis() + deadlineMillis
;
158 setInstance(attributes
, instance
);
159 setPort(attributes
, port
);
160 requestEndListeners
=
161 Collections
.newSetFromMap(new ConcurrentHashMap
<RequestEndListener
, Boolean
>(10));
162 attributes
.put(REQUEST_ID
, generateRequestId());
163 attributes
.put(REQUEST_END_LISTENERS
, requestEndListeners
);
164 attributes
.put(START_TIME_ATTR
, new Date());
165 attributes
.put(REQUEST_THREAD_FACTORY_ATTR
, new RequestThreadFactory());
166 attributes
.put(BACKGROUND_THREAD_FACTORY_ATTR
, new BackgroundThreadFactory(appId
,
167 moduleName
, majorVersionId
));
171 * Sets the instance for the provided attributes.
173 static void setInstance(Map
<String
, Object
> attributes
, int instance
) {
174 attributes
.remove(INSTANCE_ID_ENV_ATTRIBUTE
);
175 if (instance
!= LocalEnvironment
.MAIN_INSTANCE
) {
176 attributes
.put(INSTANCE_ID_ENV_ATTRIBUTE
, Integer
.toString(instance
));
181 * Sets the {@link #PORT_ID_ENV_ATTRIBUTE} value to the provided port value or
182 * clears it if port is null.
184 static void setPort(Map
<String
, Object
> attributes
, Integer port
) {
186 attributes
.remove(PORT_ID_ENV_ATTRIBUTE
);
188 attributes
.put(PORT_ID_ENV_ATTRIBUTE
, port
);
193 * Returns the current instance or {@link #MAIN_INSTANCE} if none is defined.
195 static int getCurrentInstance() {
196 int result
= MAIN_INSTANCE
;
197 String instance
= (String
) ApiProxy
.getCurrentEnvironment().getAttributes().get(
198 LocalEnvironment
.INSTANCE_ID_ENV_ATTRIBUTE
);
199 if (instance
!= null) {
200 result
= Integer
.parseInt(instance
);
206 * Returns the current instance's port or null if none is defined.
208 static Integer
getCurrentPort() {
209 return (Integer
) ApiProxy
.getCurrentEnvironment().getAttributes().get(
210 LocalEnvironment
.PORT_ID_ENV_ATTRIBUTE
);
213 private static AtomicInteger requestID
= new AtomicInteger();
216 * Generates a unique request ID using the same algorithm as the Python dev
217 * appserver. It is similar to the production algorithm, but with less
218 * embedded data. The primary goal is that the IDs be sortable by timestamp,
219 * so the initial bytes consist of seconds then microseconds packed in
220 * big-endian byte order. To ensure uniqueness, a hash of the incrementing
221 * counter is appended. Hexadecimal encoding is used instead of base64 in
222 * order to preserve comparison order.
224 private String
generateRequestId(){
226 ByteBuffer buf
= ByteBuffer
.allocate(12);
228 long now
= System
.currentTimeMillis();
229 buf
.putInt((int) (now
/ 1000));
230 buf
.putInt((int) ((now
* 1000) % 1000000));
232 String nextID
= new Integer(requestID
.getAndIncrement()).toString();
234 MessageDigest
.getInstance("SHA-1").digest(nextID
.getBytes(Charsets
.US_ASCII
));
235 buf
.put(hashBytes
, 0, 4);
237 return String
.format("%x", new BigInteger(buf
.array()));
238 } catch (Exception e
) {
244 public String
getAppId() {
249 public String
getVersionId() {
254 public String
getAuthDomain() {
260 public final String
getRequestNamespace() {
261 String appsNamespace
= (String
) getAttributes().get(APPS_NAMESPACE_KEY
);
262 return appsNamespace
== null ?
"" : appsNamespace
;
266 public ConcurrentMap
<String
, Object
> getAttributes() {
270 public void callRequestEndListeners() {
271 for (RequestEndListener listener
: requestEndListeners
) {
273 listener
.onRequestEnd(this);
274 } catch (Exception ex
) {
275 logger
.log(Level
.WARNING
,
276 "Exception while attempting to invoke RequestEndListener " + listener
.getClass()
280 requestEndListeners
.clear();
284 public long getRemainingMillis() {
285 if (endTime
!= null) {
286 return endTime
- System
.currentTimeMillis();
289 return Long
.MAX_VALUE
;
293 * Returns the major version component from the provided version id or null
294 * if the provided version id has no major version component.
296 public static String
getMajorVersion(String versionId
) {
297 Matcher matcher
= APP_ID_PATTERN
.matcher(versionId
);
299 return matcher
.group(3) == null ? matcher
.group(1) : matcher
.group(3);
303 * Returns the module component from the provided version id or
304 * {@link WebModule#DEFAULT_MODULE_NAME} if the provided version id has no
307 public static String
getModuleName(String versionId
) {
308 Matcher matcher
= APP_ID_PATTERN
.matcher(versionId
);
310 return matcher
.group(3) == null ? WebModule
.DEFAULT_MODULE_NAME
: matcher
.group(1);