| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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 org.apache.solr.client.solrj.impl; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.lang.invoke.MethodHandles; |
| import java.lang.reflect.InvocationTargetException; |
| import java.util.List; |
| import java.util.Optional; |
| import java.util.concurrent.CopyOnWriteArrayList; |
| import java.util.concurrent.TimeUnit; |
| import java.util.function.Consumer; |
| import java.util.zip.GZIPInputStream; |
| import java.util.zip.InflaterInputStream; |
| |
| import org.apache.http.Header; |
| import org.apache.http.HeaderElement; |
| import org.apache.http.HttpEntity; |
| import org.apache.http.HttpException; |
| import org.apache.http.HttpRequest; |
| import org.apache.http.HttpRequestInterceptor; |
| import org.apache.http.HttpResponse; |
| import org.apache.http.HttpResponseInterceptor; |
| import org.apache.http.auth.AuthScope; |
| import org.apache.http.auth.UsernamePasswordCredentials; |
| import org.apache.http.client.CredentialsProvider; |
| import org.apache.http.client.HttpClient; |
| import org.apache.http.client.config.RequestConfig; |
| import org.apache.http.client.config.RequestConfig.Builder; |
| import org.apache.http.client.protocol.HttpClientContext; |
| import org.apache.http.config.Registry; |
| import org.apache.http.config.RegistryBuilder; |
| import org.apache.http.conn.ConnectionKeepAliveStrategy; |
| import org.apache.http.conn.socket.ConnectionSocketFactory; |
| import org.apache.http.conn.socket.PlainConnectionSocketFactory; |
| import org.apache.http.conn.ssl.NoopHostnameVerifier; |
| import org.apache.http.conn.ssl.SSLConnectionSocketFactory; |
| import org.apache.http.entity.HttpEntityWrapper; |
| import org.apache.http.impl.client.BasicCredentialsProvider; |
| import org.apache.http.impl.client.CloseableHttpClient; |
| import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; |
| import org.apache.http.impl.client.HttpClientBuilder; |
| import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; |
| import org.apache.http.protocol.HttpContext; |
| import org.apache.http.protocol.HttpRequestExecutor; |
| import org.apache.http.ssl.SSLContexts; |
| import org.apache.solr.common.params.ModifiableSolrParams; |
| import org.apache.solr.common.params.SolrParams; |
| import org.apache.solr.common.util.IOUtils; |
| import org.apache.solr.common.util.ObjectReleaseTracker; |
| import org.apache.solr.common.util.Utils; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * Utility class for creating/configuring httpclient instances. |
| * |
| * This class can touch internal HttpClient details and is subject to change. |
| * |
| * @lucene.experimental |
| */ |
| public class HttpClientUtil { |
| |
| private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); |
| |
| public static final int DEFAULT_CONNECT_TIMEOUT = 60000; |
| public static final int DEFAULT_SO_TIMEOUT = 600000; |
| public static final int DEFAULT_MAXCONNECTIONSPERHOST = 100000; |
| public static final int DEFAULT_MAXCONNECTIONS = 100000; |
| |
| private static final int VALIDATE_AFTER_INACTIVITY_DEFAULT = 3000; |
| private static final int EVICT_IDLE_CONNECTIONS_DEFAULT = 50000; |
| private static final String VALIDATE_AFTER_INACTIVITY = "validateAfterInactivity"; |
| private static final String EVICT_IDLE_CONNECTIONS = "evictIdleConnections"; |
| |
| // Maximum connections allowed per host |
| public static final String PROP_MAX_CONNECTIONS_PER_HOST = "maxConnectionsPerHost"; |
| // Maximum total connections allowed |
| public static final String PROP_MAX_CONNECTIONS = "maxConnections"; |
| // Retry http requests on error |
| public static final String PROP_USE_RETRY = "retry"; |
| // Allow compression (deflate,gzip) if server supports it |
| public static final String PROP_ALLOW_COMPRESSION = "allowCompression"; |
| // Basic auth username |
| public static final String PROP_BASIC_AUTH_USER = "httpBasicAuthUser"; |
| // Basic auth password |
| public static final String PROP_BASIC_AUTH_PASS = "httpBasicAuthPassword"; |
| |
| /** |
| * System property consulted to determine if the default {@link SocketFactoryRegistryProvider} |
| * will require hostname validation of SSL Certificates. The default behavior is to enforce |
| * peer name validation. |
| * <p> |
| * This property will have no effect if {@link #setSocketFactoryRegistryProvider} is used to override |
| * the default {@link SocketFactoryRegistryProvider} |
| * </p> |
| */ |
| public static final String SYS_PROP_CHECK_PEER_NAME = "solr.ssl.checkPeerName"; |
| |
| // * NOTE* The following params configure the default request config and this |
| // is overridden by SolrJ clients. Use the setters on the SolrJ clients to |
| // to configure these settings if that is the intent. |
| |
| // Follow redirects |
| public static final String PROP_FOLLOW_REDIRECTS = "followRedirects"; |
| |
| // socket timeout measured in ms, closes a socket if read |
| // takes longer than x ms to complete. throws |
| // java.net.SocketTimeoutException: Read timed out exception |
| public static final String PROP_SO_TIMEOUT = "socketTimeout"; |
| // connection timeout measures in ms, closes a socket if connection |
| // cannot be established within x ms. with a |
| // java.net.SocketTimeoutException: Connection timed out |
| public static final String PROP_CONNECTION_TIMEOUT = "connTimeout"; |
| |
| /** |
| * A Java system property to select the {@linkplain HttpClientBuilderFactory} used for |
| * configuring the {@linkplain HttpClientBuilder} instance by default. |
| */ |
| public static final String SYS_PROP_HTTP_CLIENT_BUILDER_FACTORY = "solr.httpclient.builder.factory"; |
| |
| /** |
| * A Java system property to select the {@linkplain SocketFactoryRegistryProvider} used for |
| * configuring the Apache HTTP clients. |
| */ |
| public static final String SYS_PROP_SOCKET_FACTORY_REGISTRY_PROVIDER = "solr.httpclient.socketFactory.registry.provider"; |
| |
| static final DefaultHttpRequestRetryHandler NO_RETRY = new DefaultHttpRequestRetryHandler( |
| 0, false); |
| |
| private static volatile SolrHttpClientBuilder httpClientBuilder; |
| |
| private static SolrHttpClientContextBuilder httpClientRequestContextBuilder = new SolrHttpClientContextBuilder(); |
| |
| private static volatile SocketFactoryRegistryProvider socketFactoryRegistryProvider; |
| private static volatile String cookiePolicy; |
| private static final List<HttpRequestInterceptor> interceptors = new CopyOnWriteArrayList<>(); |
| |
| |
| static { |
| resetHttpClientBuilder(); |
| |
| // Configure the SocketFactoryRegistryProvider if user has specified the provider type. |
| String socketFactoryRegistryProviderClassName = System.getProperty(SYS_PROP_SOCKET_FACTORY_REGISTRY_PROVIDER); |
| if (socketFactoryRegistryProviderClassName != null) { |
| log.debug("Using {}", socketFactoryRegistryProviderClassName); |
| try { |
| socketFactoryRegistryProvider = (SocketFactoryRegistryProvider)Class.forName(socketFactoryRegistryProviderClassName).getConstructor().newInstance(); |
| } catch (InstantiationException | IllegalAccessException | ClassNotFoundException | InvocationTargetException | NoSuchMethodException e) { |
| throw new RuntimeException("Unable to instantiate Solr SocketFactoryRegistryProvider", e); |
| } |
| } |
| |
| // Configure the HttpClientBuilder if user has specified the factory type. |
| String factoryClassName = System.getProperty(SYS_PROP_HTTP_CLIENT_BUILDER_FACTORY); |
| if (factoryClassName != null) { |
| log.debug ("Using {}", factoryClassName); |
| try { |
| HttpClientBuilderFactory factory = (HttpClientBuilderFactory)Class.forName(factoryClassName).newInstance(); |
| httpClientBuilder = factory.getHttpClientBuilder(Optional.of(SolrHttpClientBuilder.create())); |
| } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) { |
| throw new RuntimeException("Unable to instantiate Solr HttpClientBuilderFactory", e); |
| } |
| } |
| } |
| |
| public static abstract class SocketFactoryRegistryProvider { |
| /** Must be non-null */ |
| public abstract Registry<ConnectionSocketFactory> getSocketFactoryRegistry(); |
| } |
| |
| private static class DynamicInterceptor implements HttpRequestInterceptor { |
| |
| @Override |
| public void process(HttpRequest request, HttpContext context) throws HttpException, IOException { |
| // don't synchronize traversal - can lead to deadlock - CopyOnWriteArrayList is critical |
| // we also do not want to have to acquire the mutex when the list is empty or put a global |
| // mutex around the process calls |
| interceptors.forEach(new Consumer<HttpRequestInterceptor>() { |
| |
| @Override |
| public void accept(HttpRequestInterceptor interceptor) { |
| try { |
| interceptor.process(request, context); |
| } catch (Exception e) { |
| log.error("", e); |
| } |
| } |
| }); |
| |
| } |
| } |
| |
| public static void setHttpClientBuilder(SolrHttpClientBuilder newHttpClientBuilder) { |
| httpClientBuilder = newHttpClientBuilder; |
| } |
| |
| public static void setHttpClientProvider(SolrHttpClientBuilder newHttpClientBuilder) { |
| httpClientBuilder = newHttpClientBuilder; |
| } |
| |
| /** |
| * @see #SYS_PROP_CHECK_PEER_NAME |
| */ |
| public static void setSocketFactoryRegistryProvider(SocketFactoryRegistryProvider newRegistryProvider) { |
| socketFactoryRegistryProvider = newRegistryProvider; |
| } |
| |
| public static SolrHttpClientBuilder getHttpClientBuilder() { |
| return httpClientBuilder; |
| } |
| |
| /** |
| * @see #SYS_PROP_CHECK_PEER_NAME |
| */ |
| public static SocketFactoryRegistryProvider getSocketFactoryRegistryProvider() { |
| return socketFactoryRegistryProvider; |
| } |
| |
| public static void resetHttpClientBuilder() { |
| socketFactoryRegistryProvider = new DefaultSocketFactoryRegistryProvider(); |
| httpClientBuilder = SolrHttpClientBuilder.create(); |
| } |
| |
| private static final class DefaultSocketFactoryRegistryProvider extends SocketFactoryRegistryProvider { |
| @Override |
| public Registry<ConnectionSocketFactory> getSocketFactoryRegistry() { |
| // this mimics PoolingHttpClientConnectionManager's default behavior, |
| // except that we explicitly use SSLConnectionSocketFactory.getSystemSocketFactory() |
| // to pick up the system level default SSLContext (where javax.net.ssl.* properties |
| // related to keystore & truststore are specified) |
| RegistryBuilder<ConnectionSocketFactory> builder = RegistryBuilder.<ConnectionSocketFactory> create(); |
| builder.register("http", PlainConnectionSocketFactory.getSocketFactory()); |
| |
| // logic to turn off peer host check |
| SSLConnectionSocketFactory sslConnectionSocketFactory = null; |
| boolean sslCheckPeerName = toBooleanDefaultIfNull( |
| toBooleanObject(System.getProperty(HttpClientUtil.SYS_PROP_CHECK_PEER_NAME)), true); |
| if (sslCheckPeerName) { |
| sslConnectionSocketFactory = SSLConnectionSocketFactory.getSystemSocketFactory(); |
| } else { |
| sslConnectionSocketFactory = new SSLConnectionSocketFactory(SSLContexts.createSystemDefault(), |
| NoopHostnameVerifier.INSTANCE); |
| log.debug("{} is false, hostname checks disabled.", HttpClientUtil.SYS_PROP_CHECK_PEER_NAME); |
| } |
| builder.register("https", sslConnectionSocketFactory); |
| |
| return builder.build(); |
| } |
| } |
| |
| /** |
| * Creates new http client by using the provided configuration. |
| * |
| * @param params |
| * http client configuration, if null a client with default |
| * configuration (no additional configuration) is created. |
| */ |
| public static CloseableHttpClient createClient(SolrParams params) { |
| return createClient(params, createPoolingConnectionManager()); |
| } |
| |
| /** test usage subject to change @lucene.experimental */ |
| static PoolingHttpClientConnectionManager createPoolingConnectionManager() { |
| return new PoolingHttpClientConnectionManager(socketFactoryRegistryProvider.getSocketFactoryRegistry()); |
| } |
| |
| public static CloseableHttpClient createClient(SolrParams params, PoolingHttpClientConnectionManager cm) { |
| if (params == null) { |
| params = new ModifiableSolrParams(); |
| } |
| |
| return createClient(params, cm, false); |
| } |
| |
| public static CloseableHttpClient createClient(final SolrParams params, PoolingHttpClientConnectionManager cm, boolean sharedConnectionManager, HttpRequestExecutor httpRequestExecutor) { |
| final ModifiableSolrParams config = new ModifiableSolrParams(params); |
| if (log.isDebugEnabled()) { |
| log.debug("Creating new http client, config: {}", config); |
| } |
| |
| cm.setMaxTotal(params.getInt(HttpClientUtil.PROP_MAX_CONNECTIONS, 10000)); |
| cm.setDefaultMaxPerRoute(params.getInt(HttpClientUtil.PROP_MAX_CONNECTIONS_PER_HOST, 10000)); |
| cm.setValidateAfterInactivity(Integer.getInteger(VALIDATE_AFTER_INACTIVITY, VALIDATE_AFTER_INACTIVITY_DEFAULT)); |
| |
| |
| HttpClientBuilder newHttpClientBuilder = HttpClientBuilder.create(); |
| |
| if (sharedConnectionManager) { |
| newHttpClientBuilder.setConnectionManagerShared(true); |
| } else { |
| newHttpClientBuilder.setConnectionManagerShared(false); |
| } |
| |
| ConnectionKeepAliveStrategy keepAliveStrat = new ConnectionKeepAliveStrategy() { |
| @Override |
| public long getKeepAliveDuration(HttpResponse response, HttpContext context) { |
| // we only close connections based on idle time, not ttl expiration |
| return -1; |
| } |
| }; |
| |
| if (httpClientBuilder.getAuthSchemeRegistryProvider() != null) { |
| newHttpClientBuilder.setDefaultAuthSchemeRegistry(httpClientBuilder.getAuthSchemeRegistryProvider().getAuthSchemeRegistry()); |
| } |
| if (httpClientBuilder.getCookieSpecRegistryProvider() != null) { |
| newHttpClientBuilder.setDefaultCookieSpecRegistry(httpClientBuilder.getCookieSpecRegistryProvider().getCookieSpecRegistry()); |
| } |
| if (httpClientBuilder.getCredentialsProviderProvider() != null) { |
| newHttpClientBuilder.setDefaultCredentialsProvider(httpClientBuilder.getCredentialsProviderProvider().getCredentialsProvider()); |
| } |
| |
| newHttpClientBuilder.addInterceptorLast(new DynamicInterceptor()); |
| |
| newHttpClientBuilder = newHttpClientBuilder.setKeepAliveStrategy(keepAliveStrat) |
| .evictIdleConnections((long) Integer.getInteger(EVICT_IDLE_CONNECTIONS, EVICT_IDLE_CONNECTIONS_DEFAULT), TimeUnit.MILLISECONDS); |
| |
| if (httpRequestExecutor != null) { |
| newHttpClientBuilder.setRequestExecutor(httpRequestExecutor); |
| } |
| |
| HttpClientBuilder builder = setupBuilder(newHttpClientBuilder, params); |
| |
| HttpClient httpClient = builder.setConnectionManager(cm).build(); |
| |
| assert ObjectReleaseTracker.track(httpClient); |
| return (CloseableHttpClient) httpClient; |
| } |
| |
| /** |
| * Creates new http client by using the provided configuration. |
| * |
| */ |
| public static CloseableHttpClient createClient(final SolrParams params, PoolingHttpClientConnectionManager cm, boolean sharedConnectionManager) { |
| return createClient(params, cm, sharedConnectionManager, null); |
| } |
| |
| private static HttpClientBuilder setupBuilder(HttpClientBuilder builder, SolrParams config) { |
| |
| Builder requestConfigBuilder = RequestConfig.custom() |
| .setRedirectsEnabled(config.getBool(HttpClientUtil.PROP_FOLLOW_REDIRECTS, false)).setDecompressionEnabled(false) |
| .setConnectTimeout(config.getInt(HttpClientUtil.PROP_CONNECTION_TIMEOUT, DEFAULT_CONNECT_TIMEOUT)) |
| .setSocketTimeout(config.getInt(HttpClientUtil.PROP_SO_TIMEOUT, DEFAULT_SO_TIMEOUT)); |
| |
| String cpolicy = cookiePolicy; |
| if (cpolicy != null) { |
| requestConfigBuilder.setCookieSpec(cpolicy); |
| } |
| |
| RequestConfig requestConfig = requestConfigBuilder.build(); |
| |
| HttpClientBuilder retBuilder = builder.setDefaultRequestConfig(requestConfig); |
| |
| if (config.getBool(HttpClientUtil.PROP_USE_RETRY, true)) { |
| retBuilder = retBuilder.setRetryHandler(new SolrHttpRequestRetryHandler(Integer.getInteger("solr.httpclient.retries", 3))); |
| |
| } else { |
| retBuilder = retBuilder.setRetryHandler(NO_RETRY); |
| } |
| |
| final String basicAuthUser = config.get(HttpClientUtil.PROP_BASIC_AUTH_USER); |
| final String basicAuthPass = config.get(HttpClientUtil.PROP_BASIC_AUTH_PASS); |
| |
| if (basicAuthUser != null && basicAuthPass != null) { |
| CredentialsProvider credsProvider = new BasicCredentialsProvider(); |
| credsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(basicAuthUser, basicAuthPass)); |
| retBuilder.setDefaultCredentialsProvider(credsProvider); |
| } |
| |
| if (config.getBool(HttpClientUtil.PROP_ALLOW_COMPRESSION, false)) { |
| retBuilder.addInterceptorFirst(new UseCompressionRequestInterceptor()); |
| retBuilder.addInterceptorFirst(new UseCompressionResponseInterceptor()); |
| } else { |
| retBuilder.disableContentCompression(); |
| } |
| |
| return retBuilder; |
| } |
| |
| public static void close(HttpClient httpClient) { |
| |
| org.apache.solr.common.util.IOUtils.closeQuietly((CloseableHttpClient) httpClient); |
| |
| assert ObjectReleaseTracker.release(httpClient); |
| } |
| |
| public static void addRequestInterceptor(HttpRequestInterceptor interceptor) { |
| interceptors.add(interceptor); |
| } |
| |
| public static void removeRequestInterceptor(HttpRequestInterceptor interceptor) { |
| interceptors.remove(interceptor); |
| } |
| |
| public static void clearRequestInterceptors() { |
| interceptors.clear(); |
| } |
| |
| private static class UseCompressionRequestInterceptor implements |
| HttpRequestInterceptor { |
| |
| @Override |
| public void process(HttpRequest request, HttpContext context) |
| throws HttpException, IOException { |
| if (!request.containsHeader("Accept-Encoding")) { |
| request.addHeader("Accept-Encoding", "gzip, deflate"); |
| } |
| } |
| } |
| |
| private static class UseCompressionResponseInterceptor implements |
| HttpResponseInterceptor { |
| |
| @Override |
| public void process(final HttpResponse response, final HttpContext context) |
| throws HttpException, IOException { |
| |
| HttpEntity entity = response.getEntity(); |
| Header ceheader = entity.getContentEncoding(); |
| if (ceheader != null) { |
| HeaderElement[] codecs = ceheader.getElements(); |
| for (int i = 0; i < codecs.length; i++) { |
| if (codecs[i].getName().equalsIgnoreCase("gzip")) { |
| response |
| .setEntity(new GzipDecompressingEntity(response.getEntity())); |
| return; |
| } |
| if (codecs[i].getName().equalsIgnoreCase("deflate")) { |
| response.setEntity(new DeflateDecompressingEntity(response |
| .getEntity())); |
| return; |
| } |
| } |
| } |
| } |
| } |
| |
| protected static class GzipDecompressingEntity extends HttpEntityWrapper { |
| private boolean gzipInputStreamCreated = false; |
| private InputStream gzipInputStream = null; |
| |
| public GzipDecompressingEntity(final HttpEntity entity) { |
| super(entity); |
| } |
| |
| /** |
| * Return a InputStream of uncompressed data. |
| * If there is an issue with the compression of the data, a null InputStream will be returned, |
| * and the underlying compressed InputStream will be closed. |
| * |
| * The same input stream will be returned if the underlying entity is not repeatable. |
| * If the underlying entity is repeatable, then a new input stream will be created. |
| */ |
| @Override |
| public InputStream getContent() throws IOException, IllegalStateException { |
| if (!gzipInputStreamCreated || wrappedEntity.isRepeatable()) { |
| gzipInputStreamCreated = true; |
| InputStream wrappedContent = wrappedEntity.getContent(); |
| if (wrappedContent != null) { |
| try { |
| gzipInputStream = new GZIPInputStream(wrappedContent); |
| } catch (IOException ioException) { |
| try { |
| Utils.readFully(wrappedContent); |
| } catch (IOException ignored) { |
| } finally { |
| IOUtils.closeQuietly(wrappedContent); |
| } |
| throw new IOException("Cannot open GZipInputStream for response", ioException); |
| } |
| } |
| } |
| return gzipInputStream; |
| } |
| |
| @Override |
| public long getContentLength() { |
| return -1; |
| } |
| } |
| |
| private static class DeflateDecompressingEntity extends |
| GzipDecompressingEntity { |
| public DeflateDecompressingEntity(final HttpEntity entity) { |
| super(entity); |
| } |
| |
| @Override |
| public InputStream getContent() throws IOException, IllegalStateException { |
| // InflaterInputStream does not throw a ZipException in the constructor, |
| // so it does not need the same checks as the GZIPInputStream. |
| return new InflaterInputStream(wrappedEntity.getContent()); |
| } |
| } |
| |
| public static void setHttpClientRequestContextBuilder(SolrHttpClientContextBuilder httpClientContextBuilder) { |
| httpClientRequestContextBuilder = httpClientContextBuilder; |
| } |
| |
| /** |
| * Create a HttpClientContext object and {@link HttpClientContext#setUserToken(Object)} |
| * to an internal singleton. It allows to reuse underneath {@link HttpClient} |
| * in connection pools if client authentication is enabled. |
| */ |
| public static HttpClientContext createNewHttpClientRequestContext() { |
| HttpClientContext context = httpClientRequestContextBuilder.createContext(HttpSolrClient.cacheKey); |
| |
| return context; |
| } |
| |
| public static Builder createDefaultRequestConfigBuilder() { |
| String cpolicy = cookiePolicy; |
| Builder builder = RequestConfig.custom(); |
| |
| builder.setSocketTimeout(DEFAULT_SO_TIMEOUT) |
| .setConnectTimeout(DEFAULT_CONNECT_TIMEOUT) |
| .setRedirectsEnabled(false) |
| .setDecompressionEnabled(false); // we do our own compression / decompression |
| if (cpolicy != null) { |
| builder.setCookieSpec(cpolicy); |
| } |
| return builder; |
| } |
| |
| public static void setCookiePolicy(String policyName) { |
| cookiePolicy = policyName; |
| } |
| |
| /** |
| * @lucene.internal |
| */ |
| static boolean toBooleanDefaultIfNull(Boolean bool, boolean valueIfNull) { |
| if (bool == null) { |
| return valueIfNull; |
| } |
| return bool.booleanValue() ? true : false; |
| } |
| |
| /** |
| * @lucene.internal |
| */ |
| static Boolean toBooleanObject(String str) { |
| if ("true".equalsIgnoreCase(str)) { |
| return Boolean.TRUE; |
| } else if ("false".equalsIgnoreCase(str)) { |
| return Boolean.FALSE; |
| } |
| // no match |
| return null; |
| } |
| } |