diff --git a/README.md b/README.md index 0438255c30d..1bc9f498621 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ `$ ant clean dist` - - To create maven artifacts (after building .jar using ant), use [following guide](./maven/README.txt). + - To create maven artifacts (after building .jar using ant), use [following guide](./maven/README.md). ### How to verify GWT code conventions: diff --git a/user/build.xml b/user/build.xml index 2af156012ff..16282cdad22 100755 --- a/user/build.xml +++ b/user/build.xml @@ -163,6 +163,17 @@ + + + + + + + + + + + @@ -178,6 +189,7 @@ + diff --git a/user/src/com/google/gwt/user/server/rpc/AbstractRemoteServiceServlet.java b/user/src/com/google/gwt/user/server/rpc/AbstractRemoteServiceServlet.java index a21e4fb7a3f..bca9fc8d276 100644 --- a/user/src/com/google/gwt/user/server/rpc/AbstractRemoteServiceServlet.java +++ b/user/src/com/google/gwt/user/server/rpc/AbstractRemoteServiceServlet.java @@ -19,7 +19,6 @@ import java.io.IOException; -import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -41,9 +40,8 @@ public AbstractRemoteServiceServlet() { * Standard HttpServlet method: handle the POST. Delegates to * {@link #processPost(HttpServletRequest, HttpServletResponse)}. * - * This doPost method swallows ALL exceptions, logs them in the - * ServletContext, and returns a GENERIC_FAILURE_MSG response with status code - * 500. + * This doPost method swallows ALL exceptions, logs them, + * and returns a GENERIC_FAILURE_MSG response with status code 500. */ @Override public final void doPost(HttpServletRequest request, @@ -106,9 +104,7 @@ protected void doUnexpectedFailure(Throwable e) { */ throw new RuntimeException("Unable to report failure", e); } - ServletContext servletContext = getServletContext(); - RPCServletUtils.writeResponseForUnexpectedFailure(servletContext, - getThreadLocalResponse(), e); + RPCServletUtils.writeResponseForUnexpectedFailure(getThreadLocalResponse(), e); } /** diff --git a/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java b/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java index 8a2a50fb012..30cd3eb5327 100644 --- a/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java +++ b/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java @@ -15,6 +15,8 @@ */ package com.google.gwt.user.server.rpc; +import com.google.gwt.user.server.rpc.logging.RpcLogManager; +import com.google.gwt.user.server.rpc.logging.RpcLogger; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -37,6 +39,8 @@ */ public class RPCServletUtils { + private static final RpcLogger logger = RpcLogManager.getLogger(RPCServletUtils.class); + public static final String CHARSET_UTF8_NAME = "UTF-8"; /** @@ -62,9 +66,11 @@ public class RPCServletUtils { private static final String CONTENT_ENCODING_GZIP = "gzip"; - private static final String CONTENT_TYPE_APPLICATION_JSON_UTF8 = "application/json; charset=utf-8"; + private static final String CONTENT_TYPE_APPLICATION_JSON_UTF8 = + "application/json; charset=utf-8"; - private static final String GENERIC_FAILURE_MSG = "The call failed on the server; see server log for details"; + private static final String GENERIC_FAILURE_MSG = + "The call failed on the server; see server log for details"; private static final String GWT_RPC_CONTENT_TYPE = "text/x-gwt-rpc"; @@ -319,12 +325,21 @@ public static boolean shouldGzipResponseContent(HttpServletRequest request, && exceedsUncompressedContentLengthLimit(responseContent); } + /** + * @deprecated Use {@link #writeResponse(HttpServletResponse, String, boolean)} instead; the + * servlet context is no longer needed. + */ + @Deprecated + public static void writeResponse(ServletContext ignored, HttpServletResponse response, + String responseContent, boolean gzipResponse) throws IOException { + writeResponse(response, responseContent, gzipResponse); + } + /** * Write the response content into the {@link HttpServletResponse}. If * gzipResponse is true, the response content will * be gzipped prior to being written into the response. * - * @param servletContext servlet context for this response * @param response response instance * @param responseContent a string containing the response content * @param gzipResponse if true the response content will be gzip @@ -332,9 +347,8 @@ public static boolean shouldGzipResponseContent(HttpServletRequest request, * @throws IOException if reading, writing, or closing the response's output * stream fails */ - public static void writeResponse(ServletContext servletContext, - HttpServletResponse response, String responseContent, boolean gzipResponse) - throws IOException { + public static void writeResponse(HttpServletResponse response, String responseContent, + boolean gzipResponse) throws IOException { byte[] responseBytes = responseContent.getBytes(StandardCharsets.UTF_8); if (gzipResponse) { @@ -363,7 +377,7 @@ public static void writeResponse(ServletContext servletContext, } if (caught != null) { - servletContext.log("Unable to compress response", caught); + logger.error("Unable to compress response", caught); response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return; } @@ -378,18 +392,26 @@ public static void writeResponse(ServletContext servletContext, response.getOutputStream().write(responseBytes); } + /** + * @deprecated Use {@link #writeResponseForUnexpectedFailure(HttpServletResponse, Throwable)} + * instead; the servlet context is no longer needed. + */ + @Deprecated + public static void writeResponseForUnexpectedFailure(ServletContext ignored, + HttpServletResponse response, Throwable failure) { + writeResponseForUnexpectedFailure(response, failure); + } + /** * Called when the servlet itself has a problem, rather than the invoked * third-party method. It writes a simple 500 message back to the client. * - * @param servletContext * @param response * @param failure */ - public static void writeResponseForUnexpectedFailure( - ServletContext servletContext, HttpServletResponse response, + public static void writeResponseForUnexpectedFailure(HttpServletResponse response, Throwable failure) { - servletContext.log("Exception while dispatching incoming RPC call", failure); + logger.error("Exception while dispatching incoming RPC call", failure); // Send GENERIC_FAILURE_MSG with 500 status. // @@ -403,7 +425,7 @@ public static void writeResponseForUnexpectedFailure( response.getWriter().write(GENERIC_FAILURE_MSG); } } catch (IOException ex) { - servletContext.log( + logger.error( "respondWithUnexpectedFailure failed while sending the previous failure to the client", ex); } @@ -431,7 +453,8 @@ private static void checkCharacterEncodingIgnoreCase( * properly parsed character encoding string if we decide to make this * change. */ - if (characterEncoding.toLowerCase(Locale.ROOT).contains(expectedCharSet.toLowerCase(Locale.ROOT))) { + if (characterEncoding.toLowerCase(Locale.ROOT) + .contains(expectedCharSet.toLowerCase(Locale.ROOT))) { encodingOkay = true; } } diff --git a/user/src/com/google/gwt/user/server/rpc/RemoteServiceServlet.java b/user/src/com/google/gwt/user/server/rpc/RemoteServiceServlet.java index 1bd73e142ef..28ff67d5ea1 100644 --- a/user/src/com/google/gwt/user/server/rpc/RemoteServiceServlet.java +++ b/user/src/com/google/gwt/user/server/rpc/RemoteServiceServlet.java @@ -22,6 +22,8 @@ import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException; import com.google.gwt.user.client.rpc.RpcTokenException; import com.google.gwt.user.client.rpc.SerializationException; +import com.google.gwt.user.server.rpc.logging.RpcLogManager; +import com.google.gwt.user.server.rpc.logging.RpcLogger; import java.io.IOException; import java.io.InputStream; @@ -45,6 +47,8 @@ public class RemoteServiceServlet extends AbstractRemoteServiceServlet implements SerializationPolicyProvider { + private static final RpcLogger logger = RpcLogManager.getLogger(RemoteServiceServlet.class); + /** * Loads a serialization policy stored as a servlet resource in the same * ServletContext as this servlet. Returns null if not found. @@ -62,7 +66,7 @@ static SerializationPolicy loadSerializationPolicy(HttpServlet servlet, modulePath = new URL(moduleBaseURL).getPath(); } catch (MalformedURLException ex) { // log the information, we will default - servlet.log("Malformed moduleBaseURL: " + moduleBaseURL, ex); + logger.error("Malformed moduleBaseURL: " + moduleBaseURL, ex); } } @@ -74,19 +78,20 @@ static SerializationPolicy loadSerializationPolicy(HttpServlet servlet, * this method. */ if (modulePath == null || !modulePath.startsWith(contextPath)) { - String message = "ERROR: The module path requested, " + String message = "The module path requested, " + modulePath + ", is not in the same web application as this servlet, " + contextPath - + ". Your module may not be properly configured or your client and server code maybe out of date."; - servlet.log(message); + + ". Your module may not be properly configured " + + "or your client and server code maybe out of date."; + logger.error(message); } else { // Strip off the context path from the module base URL. It should be a // strict prefix. String contextRelativePath = modulePath.substring(contextPath.length()); - String serializationPolicyFilePath = SerializationPolicyLoader.getSerializationPolicyFileName(contextRelativePath - + strongName); + String serializationPolicyFilePath = SerializationPolicyLoader.getSerializationPolicyFileName( + contextRelativePath + strongName); // Open the RPC resource file and read its contents. InputStream is = servlet.getServletContext().getResourceAsStream( @@ -98,30 +103,30 @@ static SerializationPolicy loadSerializationPolicy(HttpServlet servlet, null); if (serializationPolicy.hasClientFields()) { if (ENABLE_ENHANCED_CLASSES) { - servlet.log("WARNING: Service deserializes enhanced JPA/JDO classes, which is " + - "unsafe. See https://github.com/gwtproject/gwt/issues/9709 for more " + - "detail on the vulnerability that this presents."); + logger.warn("Service deserializes enhanced JPA/JDO classes, which is " + + "unsafe. See https://github.com/gwtproject/gwt/issues/9709 for more " + + "detail on the vulnerability that this presents."); } else { - servlet.log("ERROR: Service deserializes enhanced JPA/JDO classes, which is " + - "unsafe. Review build logs to see which classes are affected, or set " + - ENABLE_GWT_ENHANCED_CLASSES_PROPERTY + " to true to allow using this " + - "service. See https://github.com/gwtproject/gwt/issues/9709 for more " + - "detail."); + logger.error("Service deserializes enhanced JPA/JDO classes, which is " + + "unsafe. Review build logs to see which classes are affected, or set " + + ENABLE_GWT_ENHANCED_CLASSES_PROPERTY + " to true to allow using this " + + "service. See https://github.com/gwtproject/gwt/issues/9709 for more " + + "detail."); serializationPolicy = null; } } } catch (ParseException e) { - servlet.log("ERROR: Failed to parse the policy file '" + logger.error("Failed to parse the policy file '" + serializationPolicyFilePath + "'", e); } catch (IOException e) { - servlet.log("ERROR: Could not read the policy file '" + logger.error("Could not read the policy file '" + serializationPolicyFilePath + "'", e); } } else { - String message = "ERROR: The serialization policy file '" + String message = "The serialization policy file '" + serializationPolicyFilePath + "' was not found; did you forget to include it in this deployment?"; - servlet.log(message); + logger.error(message); } } finally { if (is != null) { @@ -144,7 +149,8 @@ static SerializationPolicy loadSerializationPolicy(HttpServlet servlet, * A cache of moduleBaseURL and serialization policy strong name to * {@link SerializationPolicy}. */ - private final Map serializationPolicyCache = new HashMap(); + private final Map serializationPolicyCache = + new HashMap(); /** * The implementation of the service. @@ -177,11 +183,13 @@ public RemoteServiceServlet(Object delegate) { } /** - * Overridden to load the gwt.codeserver.port system property. + * Overridden to load the gwt.codeserver.port system property and initialize the + * {@link RpcLogManager} with a provider name from system properties or the servlet config. */ @Override public void init(ServletConfig config) throws ServletException { super.init(config); + RpcLogManager.setServletContext(config.getServletContext()); codeServerPort = getCodeServerPort(); } @@ -260,12 +268,12 @@ public final SerializationPolicy getSerializationPolicy(String moduleBaseURL, if (serializationPolicy == null) { // Failed to get the requested serialization policy; use the default - log( - "WARNING: Failed to get the SerializationPolicy '" + logger.warn("Failed to get the SerializationPolicy '" + strongName + "' for module '" + moduleBaseURL - + "'; a legacy, 1.3.3 compatible, serialization policy will be used. You may experience SerializationExceptions as a result."); + + "'; a legacy, 1.3.3 compatible, serialization policy will be used. " + + "You may experience SerializationExceptions as a result."); serializationPolicy = RPC.getDefaultSerializationPolicy(); } @@ -311,7 +319,7 @@ public String processCall(String payload) throws SerializationException { try { rpcRequest = RPC.decodeRequest(payload, delegate.getClass(), this); } catch (IncompatibleRemoteServiceException ex) { - log( + logger.error( "An IncompatibleRemoteServiceException was thrown while processing this call.", ex); return RPC.encodeResponseForFailedRequest(null, ex); @@ -350,13 +358,12 @@ public String processCall(RPCRequest rpcRequest) throws SerializationException { rpcRequest.getParameters(), rpcRequest.getSerializationPolicy(), rpcRequest.getFlags()); } catch (IncompatibleRemoteServiceException ex) { - log( + logger.error( "An IncompatibleRemoteServiceException was thrown while processing this call.", ex); return RPC.encodeResponseForFailedRequest(rpcRequest, ex); } catch (RpcTokenException tokenException) { - log("An RpcTokenException was thrown while processing this call.", - tokenException); + logger.error("An RpcTokenException was thrown while processing this call."); return RPC.encodeResponseForFailedRequest(rpcRequest, tokenException); } } @@ -364,9 +371,8 @@ public String processCall(RPCRequest rpcRequest) throws SerializationException { /** * Standard HttpServlet method: handle the POST. * - * This doPost method swallows ALL exceptions, logs them in the - * ServletContext, and returns a GENERIC_FAILURE_MSG response with status code - * 500. + * This doPost method swallows ALL exceptions, logs them, + * and returns a GENERIC_FAILURE_MSG response with status code 500. * * @throws ServletException * @throws SerializationException @@ -465,19 +471,7 @@ protected String getCodeServerPolicyUrl(String strongName) { * no authentication. It should only be used during development.

*/ protected SerializationPolicy loadPolicyFromCodeServer(String url) { - SerializationPolicyClient.Logger adapter = new SerializationPolicyClient.Logger() { - - @Override - public void logInfo(String message) { - RemoteServiceServlet.this.log(message); - } - - @Override - public void logError(String message, Throwable throwable) { - RemoteServiceServlet.this.log(message, throwable); - } - }; - return CODE_SERVER_CLIENT.loadPolicy(url, adapter); + return CODE_SERVER_CLIENT.loadPolicy(url); } /** @@ -541,7 +535,6 @@ private void writeResponse(HttpServletRequest request, boolean gzipEncode = RPCServletUtils.acceptsGzipEncoding(request) && shouldCompressResponse(request, response, responsePayload); - RPCServletUtils.writeResponse(getServletContext(), response, - responsePayload, gzipEncode); + RPCServletUtils.writeResponse(response, responsePayload, gzipEncode); } } diff --git a/user/src/com/google/gwt/user/server/rpc/SerializationPolicyClient.java b/user/src/com/google/gwt/user/server/rpc/SerializationPolicyClient.java index dffc45a8841..e228d5598a2 100644 --- a/user/src/com/google/gwt/user/server/rpc/SerializationPolicyClient.java +++ b/user/src/com/google/gwt/user/server/rpc/SerializationPolicyClient.java @@ -15,6 +15,9 @@ */ package com.google.gwt.user.server.rpc; +import com.google.gwt.user.server.rpc.logging.RpcLogManager; +import com.google.gwt.user.server.rpc.logging.RpcLogger; + import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; @@ -30,11 +33,14 @@ * (Intended only for development.) */ class SerializationPolicyClient { + + private static final RpcLogger logger = RpcLogManager.getLogger(SerializationPolicyClient.class); private final int connectTimeout; private final int readTimeout; /** - * Creates an client with the given configuration, + * Creates a client to load serialization policies from a dev mode server, with the given + * timeouts. * @param connectTimeoutMs see {@link URLConnection#setConnectTimeout} * @param readTimeoutMs see {@link URLConnection#setReadTimeout} */ @@ -43,12 +49,12 @@ class SerializationPolicyClient { this.readTimeout = readTimeoutMs; } - SerializationPolicy loadPolicy(String url, Logger logger) { + SerializationPolicy loadPolicy(String url) { URL urlObj; try { urlObj = new URL(url); } catch (MalformedURLException e) { - logger.logError("Can't parse serialization policy URL: " + url, e); + logger.error("Can't parse serialization policy URL: " + url, e); return null; } @@ -61,16 +67,16 @@ SerializationPolicy loadPolicy(String url, Logger logger) { // The code server doesn't redirect. Fail fast if we get a redirect since // it's likely a configuration error. if (conn instanceof HttpURLConnection) { - ((HttpURLConnection)conn).setInstanceFollowRedirects(false); + ((HttpURLConnection) conn).setInstanceFollowRedirects(false); } conn.connect(); in = conn.getInputStream(); } catch (IOException e) { - logger.logError("Can't open serialization policy URL: " + url, e); + logger.error("Can't open serialization policy URL: " + url, e); return null; } - return readPolicy(in, url, logger); + return readPolicy(in, url); } /** @@ -79,34 +85,33 @@ SerializationPolicy loadPolicy(String url, Logger logger) { * @param sourceName names the source of the input stream for log messages. * @return the policy or null if unavailable. */ - private static SerializationPolicy readPolicy(InputStream in, String sourceName, - Logger logger) { + private static SerializationPolicy readPolicy(InputStream in, String sourceName) { try { List errs = new ArrayList(); SerializationPolicy policy = SerializationPolicyLoader.loadFromStream(in, errs); - logger.logInfo("Downloaded serialization policy from " + sourceName); + logger.info("Downloaded serialization policy from " + sourceName); if (!errs.isEmpty()) { - logMissingClasses(logger, errs); + logMissingClasses(errs); } return policy; } catch (ParseException e) { - logger.logError("Can't parse serialization policy from " + sourceName, e); + logger.error("Can't parse serialization policy from " + sourceName, e); return null; } catch (IOException e) { - logger.logError("Can't read serialization policy from " + sourceName, e); + logger.error("Can't read serialization policy from " + sourceName, e); return null; } finally { try { in.close(); } catch (IOException e) { - logger.logError("Can't close serialization policy url: " + sourceName, e); + logger.error("Can't close serialization policy url: " + sourceName, e); } } } - private static void logMissingClasses(Logger logger, List errs) { + private static void logMissingClasses(List errs) { StringBuilder message = new StringBuilder(); message.append("Unable to load server-side classes used by policy:\n"); @@ -118,14 +123,7 @@ private static void logMissingClasses(Logger logger, List 0) { message.append(" (omitted " + omitted + " more classes)\n"); } - logger.logInfo(message.toString()); + logger.info(message.toString()); } - /** - * Destination for the loader's log messages. - */ - interface Logger { - void logInfo(String message); - void logError(String message, Throwable throwable); - } } diff --git a/user/src/com/google/gwt/user/server/rpc/logging/JulLoggerProvider.java b/user/src/com/google/gwt/user/server/rpc/logging/JulLoggerProvider.java new file mode 100644 index 00000000000..7e8cc14beab --- /dev/null +++ b/user/src/com/google/gwt/user/server/rpc/logging/JulLoggerProvider.java @@ -0,0 +1,67 @@ +/* + * Copyright 2025 GWT Project Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.user.server.rpc.logging; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * An {@link RpcLoggerProvider} that delegates to {@link java.util.logging.Logger}. + * + * @see RpcLogManager + */ +public class JulLoggerProvider implements RpcLoggerProvider { + + /** + * Public for {@link java.util.ServiceLoader}; not intended for direct use outside this package. + */ + public JulLoggerProvider() { + } + + @Override + public RpcLogger createLogger(String name) { + return new JulLogger(Logger.getLogger(name)); + } + + private static final class JulLogger implements RpcLogger { + private final Logger logger; + + JulLogger(Logger logger) { + this.logger = logger; + } + + @Override + public void info(String message) { + logger.log(Level.INFO, message); + } + + @Override + public void warn(String message) { + logger.log(Level.WARNING, message); + } + + @Override + public void error(String message) { + logger.log(Level.SEVERE, message); + } + + @Override + public void error(String message, Throwable throwable) { + logger.log(Level.SEVERE, message, throwable); + } + } + +} diff --git a/user/src/com/google/gwt/user/server/rpc/logging/RpcLogManager.java b/user/src/com/google/gwt/user/server/rpc/logging/RpcLogManager.java new file mode 100644 index 00000000000..0b749816b3b --- /dev/null +++ b/user/src/com/google/gwt/user/server/rpc/logging/RpcLogManager.java @@ -0,0 +1,95 @@ +/* + * Copyright 2025 GWT Project Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.user.server.rpc.logging; + +import java.util.ServiceLoader; +import java.util.concurrent.ConcurrentHashMap; + +import javax.servlet.ServletContext; + +/** + * Entry point for obtaining {@link RpcLogger}s. Initializes the {@link RpcLoggerProvider} on class + * load using {@link ServiceLoader} to discover available implementations. + *

+ * The provider is chosen in this order: + *

    + *
  1. The first provider with a fully-qualified class name that matches the system property with + * the key {@link #PROVIDER_PROPERTY_KEY} (gwt.rpc.logging)
  2. + *
  3. The first provider for which {@link RpcLoggerProvider#isDefault()} returns + * true
  4. + *
  5. The {@link ServletContextLoggerProvider}
  6. + *
+ */ +public class RpcLogManager { + + /** + * System property key for selecting an {@link RpcLoggerProvider} by fully-qualified class name. + */ + public static final String PROVIDER_PROPERTY_KEY = "gwt.rpc.logging"; + private static final ConcurrentHashMap loggers = new ConcurrentHashMap<>(); + private static final RpcLoggerProvider loggerProvider = loadProvider(); + + /** + * Creates or retrieves a logger for the fully-qualified name of the given class. + * + * @param clazz the class for which to return a logger + * @return a logger + */ + public static RpcLogger getLogger(Class clazz) { + return loggers.computeIfAbsent(clazz.getName(), loggerProvider::createLogger); + } + + /** + * Sets the servlet context of the {@link ServletContextLoggerProvider} to use for logging. Has no + * effect if the provider is not a {@link ServletContextLoggerProvider}. + * + * @param servletContext the servlet context to use + */ + public static void setServletContext(ServletContext servletContext) { + if (loggerProvider instanceof ServletContextLoggerProvider) { + ((ServletContextLoggerProvider) loggerProvider).setServletContext(servletContext); + } + } + + /** + * Loads available providers and chooses the first whose class matches the name given with the + * {@link #PROVIDER_PROPERTY_KEY} system property, or, failing that, the first for which + * {@link RpcLoggerProvider#isDefault()} returns true. If none found, returns a + * {@link ServletContextLoggerProvider} as a fallback. + * + * @return a logger provider + */ + private static RpcLoggerProvider loadProvider() { + String providerClassName = System.getProperty(PROVIDER_PROPERTY_KEY); + ServiceLoader loaderService = ServiceLoader.load(RpcLoggerProvider.class); + for (RpcLoggerProvider provider : loaderService) { + if (provider.getClass().getName().equals(providerClassName)) { + return provider; + } + } + for (RpcLoggerProvider provider : loaderService) { + if (provider.isDefault()) { + return provider; + } + } + return new ServletContextLoggerProvider(); + } + + private RpcLogManager() { + // Not instantiable + } + +} diff --git a/user/src/com/google/gwt/user/server/rpc/logging/RpcLogger.java b/user/src/com/google/gwt/user/server/rpc/logging/RpcLogger.java new file mode 100644 index 00000000000..7958d8842d8 --- /dev/null +++ b/user/src/com/google/gwt/user/server/rpc/logging/RpcLogger.java @@ -0,0 +1,37 @@ +/* + * Copyright 2025 GWT Project Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.user.server.rpc.logging; + +/** + * A minimal logging facade used by GWT's server-side RPC package. + *

+ * Instances can be obtained from {@link RpcLogManager#getLogger(Class)}. The logging system that + * this delegates to is selected by {@link RpcLogManager} at class-initialization. + * + * @see RpcLogManager + * @see RpcLoggerProvider + */ +public interface RpcLogger { + + void info(String message); + + void warn(String message); + + void error(String message); + + void error(String message, Throwable throwable); + +} diff --git a/user/src/com/google/gwt/user/server/rpc/logging/RpcLoggerProvider.java b/user/src/com/google/gwt/user/server/rpc/logging/RpcLoggerProvider.java new file mode 100644 index 00000000000..51c9de5ac0e --- /dev/null +++ b/user/src/com/google/gwt/user/server/rpc/logging/RpcLoggerProvider.java @@ -0,0 +1,46 @@ +/* + * Copyright 2025 GWT Project Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.user.server.rpc.logging; + +/** + * Service provider interface for obtaining {@link RpcLogger} instances. + *

+ * This is not intended to be used directly, but through {@link RpcLogManager#getLogger(Class)}, + * which discovers implementations with a service loader + * + * @see RpcLogManager + */ +public interface RpcLoggerProvider { + + /** + * Creates or retrieves a logger with the given name. + * + * @param name the name of the logger to create or retrieve + * @return the created or retrieved logger + */ + RpcLogger createLogger(String name); + + /** + * Indicates whether this provider should be used as the default in the absence of a provider + * explicitly selected with the {@link RpcLogManager#PROVIDER_PROPERTY_KEY} system property. + * + * @return true if this provider should be used when no named provider is found + */ + default boolean isDefault() { + return false; + } + +} diff --git a/user/src/com/google/gwt/user/server/rpc/logging/ServletContextLoggerProvider.java b/user/src/com/google/gwt/user/server/rpc/logging/ServletContextLoggerProvider.java new file mode 100644 index 00000000000..8fee64ddce0 --- /dev/null +++ b/user/src/com/google/gwt/user/server/rpc/logging/ServletContextLoggerProvider.java @@ -0,0 +1,102 @@ +/* + * Copyright 2025 GWT Project Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.user.server.rpc.logging; + +import javax.servlet.ServletContext; + +/** + * An {@link RpcLoggerProvider} that delegates to the servlet context's logging. Used as the + * fallback if no other logger provider is found. + *

+ * Servlet context logging does not support separate named loggers, so this reuses the same logger + * instance. This reuse allows the servlet context to be easily set after the provider is + * initialized, and normally the provider will be initialized before the servlet context is even + * available. Logs are written to {@link System#out} and {@link System#err} until the servlet + * context is set. + * + * @see RpcLogManager + */ +public class ServletContextLoggerProvider implements RpcLoggerProvider { + + private final ServletContextLogger logger = new ServletContextLogger(); + + /** + * Public for {@link java.util.ServiceLoader}; not intended for direct use outside this package. + */ + public ServletContextLoggerProvider() { + } + + /** + * Sets the {@link ServletContext} to which log messages will be written. + * + * @param servletContext the servlet context to use + */ + void setServletContext(ServletContext servletContext) { + if (servletContext != null) { + logger.servletContext = servletContext; + } + } + + @Override + public RpcLogger createLogger(String ignored) { + return logger; + } + + private static final class ServletContextLogger implements RpcLogger { + + private volatile ServletContext servletContext; + + private ServletContextLogger() { } + + @Override + public void info(String message) { + if (servletContext != null) { + servletContext.log("INFO: " + message); + } else { + System.out.println("INFO: " + message); + } + } + + @Override + public void warn(String message) { + if (servletContext != null) { + servletContext.log("WARNING: " + message); + } else { + System.out.println("WARNING: " + message); + } + } + + @Override + public void error(String message) { + if (servletContext != null) { + servletContext.log("ERROR: " + message); + } else { + System.err.println("ERROR: " + message); + } + } + + @Override + public void error(String message, Throwable throwable) { + if (servletContext != null) { + servletContext.log("ERROR: " + message, throwable); + } else { + System.err.println("ERROR: " + message); + throwable.printStackTrace(); + } + } + } + +}