org.sonatype.nexus.internal.httpclient.HttpClientManagerImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.sonatype.nexus.internal.httpclient.HttpClientManagerImpl.java

Source

/*
 * Sonatype Nexus (TM) Open Source Version
 * Copyright (c) 2008-present Sonatype, Inc.
 * All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions.
 *
 * This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0,
 * which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html.
 *
 * Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks
 * of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the
 * Eclipse Foundation. All other trademarks are the property of their respective owners.
 */
package org.sonatype.nexus.internal.httpclient;

import java.net.URI;
import java.util.Map.Entry;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.Priority;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;

import org.sonatype.goodies.common.Mutex;
import org.sonatype.nexus.common.app.ManagedLifecycle;
import org.sonatype.nexus.common.event.EventAware;
import org.sonatype.nexus.common.event.EventManager;
import org.sonatype.nexus.common.stateguard.Guarded;
import org.sonatype.nexus.common.stateguard.StateGuardLifecycleSupport;
import org.sonatype.nexus.httpclient.HttpClientManager;
import org.sonatype.nexus.httpclient.HttpClientPlan;
import org.sonatype.nexus.httpclient.HttpClientPlan.Customizer;
import org.sonatype.nexus.httpclient.config.ConfigurationCustomizer;
import org.sonatype.nexus.httpclient.config.HttpClientConfiguration;
import org.sonatype.nexus.httpclient.config.HttpClientConfigurationChangedEvent;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Stopwatch;
import com.google.common.eventbus.Subscribe;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.utils.URIUtils;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.protocol.HttpContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.google.common.base.Preconditions.checkNotNull;
import static org.sonatype.nexus.common.app.ManagedLifecycle.Phase.SERVICES;
import static org.sonatype.nexus.common.stateguard.StateGuardLifecycleSupport.State.STARTED;

/**
 * Default {@link HttpClientManager}.
 *
 * @since 3.0
 */
@Named
@ManagedLifecycle(phase = SERVICES)
@Priority(Integer.MAX_VALUE) // make sure this starts early
@Singleton
public class HttpClientManagerImpl extends StateGuardLifecycleSupport implements HttpClientManager, EventAware {
    static final String HTTPCLIENT_OUTBOUND_LOGGER_NAME = "org.sonatype.nexus.httpclient.outbound";

    private static final String CTX_REQ_STOPWATCH = "request.stopwatch";

    private static final String CTX_REQ_URI = "request.uri";

    private final Logger outboundLog = LoggerFactory.getLogger(HTTPCLIENT_OUTBOUND_LOGGER_NAME);

    private final EventManager eventManager;

    private final HttpClientConfigurationStore store;

    private final Provider<HttpClientConfiguration> defaults;

    private final SharedHttpClientConnectionManager sharedConnectionManager;

    private final DefaultsCustomizer defaultsCustomizer;

    private final Mutex lock = new Mutex();

    private HttpClientConfiguration configuration;

    @Inject
    public HttpClientManagerImpl(final EventManager eventManager, final HttpClientConfigurationStore store,
            @Named("initial") final Provider<HttpClientConfiguration> defaults,
            final SharedHttpClientConnectionManager sharedConnectionManager,
            final DefaultsCustomizer defaultsCustomizer) {
        this.eventManager = checkNotNull(eventManager);

        this.store = checkNotNull(store);
        log.debug("Store: {}", store);

        this.defaults = checkNotNull(defaults);
        log.debug("Defaults: {}", defaults);

        this.sharedConnectionManager = checkNotNull(sharedConnectionManager);
        this.defaultsCustomizer = checkNotNull(defaultsCustomizer);
    }

    //
    // Lifecycle
    //

    @Override
    protected void doStart() throws Exception {
        sharedConnectionManager.start();
    }

    @Override
    protected void doStop() throws Exception {
        sharedConnectionManager.stop();
    }

    //
    // Configuration
    //

    /**
     * Load configuration from store, or use defaults.
     */
    private HttpClientConfiguration loadConfiguration() {
        HttpClientConfiguration model = store.load();

        // use defaults if no configuration was loaded from the store
        if (model == null) {
            model = defaults.get();

            // default config must not be null
            checkNotNull(model);

            log.info("Using default configuration: {}", model);
        } else {
            log.info("Loaded configuration: {}", model);
        }

        return model;
    }

    /**
     * Return configuration, loading if needed.
     *
     * The result model should be considered _immutable_ unless copied.
     */
    private HttpClientConfiguration getConfigurationInternal() {
        synchronized (lock) {
            if (configuration == null) {
                configuration = loadConfiguration();
            }
            return configuration;
        }
    }

    /**
     * Return _copy_ of configuration.
     */
    @Override
    @Guarded(by = STARTED)
    public HttpClientConfiguration getConfiguration() {
        return getConfigurationInternal().copy();
    }

    @Override
    @Guarded(by = STARTED)
    public void setConfiguration(final HttpClientConfiguration configuration) {
        checkNotNull(configuration);

        HttpClientConfiguration model = configuration.copy();
        log.info("Saving configuration: {}", model);
        synchronized (lock) {
            store.save(model);
            this.configuration = model;
        }

        eventManager.post(new HttpClientConfigurationChangedEvent(model));
    }

    @Subscribe
    public void onStoreChanged(final HttpClientConfigurationEvent event) {
        if (!event.isLocal()) {
            log.debug("Reloading configuration after change by node {}", event.getRemoteNodeId());
            HttpClientConfiguration model;
            synchronized (lock) {
                configuration = model = loadConfiguration();
            }
            eventManager.post(new HttpClientConfigurationChangedEvent(model));
        }
    }

    //
    // Instance creation
    //

    @Override
    @Guarded(by = STARTED)
    public CloseableHttpClient create(@Nullable final HttpClientPlan.Customizer customizer) {
        return prepare(customizer).build();
    }

    @Override
    @Guarded(by = STARTED)
    public CloseableHttpClient create() {
        // create with defaults only
        return create(null);
    }

    @Override
    @Guarded(by = STARTED)
    public HttpClientBuilder prepare(@Nullable final Customizer customizer) {
        final HttpClientPlan plan = httpClientPlan();

        // attach connection manager early, so customizer has chance to replace it if needed
        plan.getClient().setConnectionManager(sharedConnectionManager);

        // apply defaults
        defaultsCustomizer.customize(plan);

        // apply globals
        new ConfigurationCustomizer(getConfigurationInternal()).customize(plan);

        // apply instance customization
        if (customizer != null) {
            customizer.customize(plan);
        }

        // apply plan to builder
        HttpClientBuilder builder = plan.getClient();
        // User agent must be set here to apply to all apache http requests, including over proxies
        String userAgent = plan.getUserAgent();
        if (userAgent != null) {
            setUserAgent(builder, userAgent);
        }
        builder.setDefaultConnectionConfig(plan.getConnection().build());
        builder.setDefaultSocketConfig(plan.getSocket().build());
        builder.setDefaultRequestConfig(plan.getRequest().build());
        builder.setDefaultCredentialsProvider(plan.getCredentials());

        builder.addInterceptorFirst((HttpRequest request, HttpContext context) -> {
            // add custom http-context attributes
            for (Entry<String, Object> entry : plan.getAttributes().entrySet()) {
                // only set context attribute if not already set, to allow per request overrides
                if (context.getAttribute(entry.getKey()) == null) {
                    context.setAttribute(entry.getKey(), entry.getValue());
                }
            }

            // add custom http-request headers
            for (Entry<String, String> entry : plan.getHeaders().entrySet()) {
                request.addHeader(entry.getKey(), entry.getValue());
            }
        });
        builder.addInterceptorLast((HttpRequest httpRequest, HttpContext httpContext) -> {
            if (outboundLog.isDebugEnabled()) {
                httpContext.setAttribute(CTX_REQ_STOPWATCH, Stopwatch.createStarted());
                httpContext.setAttribute(CTX_REQ_URI, getRequestURI(httpContext));
                outboundLog.debug("{} > {}", httpContext.getAttribute(CTX_REQ_URI), httpRequest.getRequestLine());
            }
        });
        builder.addInterceptorLast((HttpResponse httpResponse, HttpContext httpContext) -> {
            Stopwatch stopwatch = (Stopwatch) httpContext.getAttribute(CTX_REQ_STOPWATCH);
            if (stopwatch != null) {
                outboundLog.debug("{} < {} @ {}", httpContext.getAttribute(CTX_REQ_URI),
                        httpResponse.getStatusLine(), stopwatch);
            }
        });

        return builder;
    }

    /**
     * Allows for verification on unverifiable final method. NOTE: if you modify the behavior of this
     * method beyond simply delegating to {@link HttpClientBuilder#setUserAgent}, write a unit test for it.
     */
    @VisibleForTesting
    void setUserAgent(HttpClientBuilder builder, String value) {
        builder.setUserAgent(value);
    }

    @VisibleForTesting
    HttpClientPlan httpClientPlan() {
        return new HttpClientPlan();
    }

    /**
     * Creates absolute request URI with full path from passed in context.
     */
    @Nonnull
    private URI getRequestURI(final HttpContext context) {
        final HttpClientContext clientContext = HttpClientContext.adapt(context);
        final HttpRequest httpRequest = clientContext.getRequest();
        final HttpHost target = clientContext.getTargetHost();
        try {
            URI uri;
            if (httpRequest instanceof HttpUriRequest) {
                uri = ((HttpUriRequest) httpRequest).getURI();
            } else {
                uri = URI.create(httpRequest.getRequestLine().getUri());
            }
            return uri.isAbsolute() ? uri : URIUtils.resolve(URI.create(target.toURI()), uri);
        } catch (Exception e) {
            log.warn("Could not create absolute request URI", e);
            return URI.create(target.toURI());
        }
    }
}