com.arpnetworking.configuration.triggers.UriTrigger Maven / Gradle / Ivy
/*
* Copyright 2015 Groupon.com
*
* 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.arpnetworking.configuration.triggers;
import com.arpnetworking.commons.builder.OvalBuilder;
import com.arpnetworking.configuration.Trigger;
import com.arpnetworking.logback.annotations.LogValue;
import com.arpnetworking.steno.LogValueMapFactory;
import com.arpnetworking.steno.Logger;
import com.arpnetworking.steno.LoggerFactory;
import net.sf.oval.constraint.NotNull;
import org.apache.http.Header;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.DateUtils;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
/**
* {@link Trigger} implementation based on a uri's last modified date and
* ETag. Either can trigger a reload; the last modified if is later than the
* previous value or the ETag if it differs from the previous value. If
* the uri is unavailable it is not considered changed to prevent flickering
* caused by connectivity or server issues.
*
* @author Ville Koskela (ville dot koskela at inscopemetrics dot com)
*/
public final class UriTrigger implements Trigger {
@Override
public boolean evaluateAndReset() {
HttpGet request = null;
try {
LOGGER.debug()
.setMessage("Evaluating trigger")
.addData("uri", _uri)
.addData("headers", _headers)
.log();
request = new HttpGet(_uri);
if (!_headers.isEmpty()) {
request.setHeaders(_headers.toArray(new Header[_headers.size()]));
}
if (_previousETag.isPresent()) {
request.addHeader(HttpHeaders.IF_NONE_MATCH, _previousETag.get());
}
if (_previousLastModified.isPresent()) {
request.addHeader(HttpHeaders.IF_MODIFIED_SINCE, DateUtils.formatDate(_previousLastModified.get()));
}
final HttpResponse response = CLIENT.execute(request);
if (response.getStatusLine().getStatusCode() == 304) {
LOGGER.debug()
.setMessage("Uri unmodified")
.addData("uri", _uri)
.addData("status", response.getStatusLine().getStatusCode())
.log();
return false;
}
if (response.getStatusLine().getStatusCode() / 100 != 2) {
LOGGER.warn()
.setMessage("Failed to retrieve url")
.addData("uri", _uri)
.addData("status", response.getStatusLine().getStatusCode())
.log();
return false;
}
if (response.getFirstHeader(HttpHeaders.ETAG) == null
&& response.getFirstHeader(HttpHeaders.LAST_MODIFIED) == null) {
LOGGER.warn()
.setMessage("Untriggerable uri missing both etag and last modified")
.addData("uri", _uri)
.addData("headers", response.getAllHeaders())
.log();
return false;
}
return isLastModifiedChanged(response) || isEtagChanged(response);
} catch (final IOException e) {
LOGGER.warn()
.setMessage("Failed to evaluate url trigger")
.addData("uri", _uri)
.setThrowable(e)
.log();
} finally {
if (request != null) {
request.releaseConnection();
}
}
return false;
}
private boolean isEtagChanged(final HttpResponse response) {
final Header newEtagHeader = response.getFirstHeader(HttpHeaders.ETAG);
if (newEtagHeader != null) {
final String newETag = newEtagHeader.getValue();
if (!_previousETag.isPresent() || !newETag.equals(_previousETag.get())) {
LOGGER.debug()
.setMessage("Uri etag changed")
.addData("uri", _uri)
.addData("newETag", newETag)
.addData("previousETag", _previousETag)
.log();
_previousETag = Optional.of(newETag);
return true;
}
}
return false;
}
private boolean isLastModifiedChanged(final HttpResponse response) {
final Header newLastModifiedHeader = response.getFirstHeader(HttpHeaders.LAST_MODIFIED);
if (newLastModifiedHeader != null) {
final Date newLastModified = DateUtils.parseDate(newLastModifiedHeader.getValue());
if (newLastModified == null) {
throw new IllegalArgumentException("Invalid last modified date");
}
if (!_previousLastModified.isPresent() || newLastModified.after(_previousLastModified.get())) {
LOGGER.debug()
.setMessage("Uri last modified changed")
.addData("uri", _uri)
.addData("newLastModified", newLastModified)
.addData("previousLastModified", _previousLastModified)
.log();
_previousLastModified = Optional.of(newLastModified);
return true;
}
}
return false;
}
/**
* Generate a Steno log compatible representation.
*
* @return Steno log compatible representation.
*/
@LogValue
public Object toLogValue() {
return LogValueMapFactory.builder(this)
.put("uri", _uri)
.put("previousLastModified", _previousLastModified)
.put("previousETag", _previousETag)
.put("headers", _headers)
.build();
}
@Override
public String toString() {
return toLogValue().toString();
}
private UriTrigger(final Builder builder) {
// The uri trigger should always return true on the first successful
// evaluation while on subsequent evaluations true should only be
// returned if the content at the uri was changed since the previous
// evaluation. To accomplish this a modified time of -1 and a null hash
// is used.
_uri = builder._uri;
_previousLastModified = Optional.empty();
_previousETag = Optional.empty();
_headers = new ArrayList<>(builder._headers);
}
private final URI _uri;
private Optional _previousLastModified;
private Optional _previousETag;
private List _headers;
private static final Logger LOGGER = LoggerFactory.getLogger(UriTrigger.class);
private static final int CONNECTION_TIMEOUT_IN_MILLISECONDS = 3000;
private static final HttpClientConnectionManager CONNECTION_MANAGER = new PoolingHttpClientConnectionManager();
private static final HttpClient CLIENT = HttpClientBuilder.create()
.setConnectionManager(CONNECTION_MANAGER)
.setDefaultRequestConfig(
RequestConfig.copy(RequestConfig.DEFAULT)
.setConnectTimeout(CONNECTION_TIMEOUT_IN_MILLISECONDS)
.build())
.build();
/**
* {@link com.arpnetworking.commons.builder.Builder} implementation for
* {@link UriTrigger}.
*/
public static final class Builder extends OvalBuilder {
/**
* Public constructor.
*/
public Builder() {
super(UriTrigger::new);
}
/**
* Set the source {@link URI}.
*
* @param value The source {@link URI}.
* @return This {@link Builder} instance.
*/
public Builder setUri(final URI value) {
_uri = value;
return this;
}
/**
* Add a {@link Header} to the uri.
*
* @param value A HTTP header.
* @return This {@link Builder} instance.
*/
public Builder addHeader(final Header value) {
_headers.add(value);
return this;
}
/**
* Add a {@link List} of {@link Header} to the uri.
*
* @param values A {@link List} of HTTP headers.
* @return This {@link Builder} instance.
*/
public Builder addHeaders(final List values) {
_headers.addAll(values);
return this;
}
/**
* Overrides the existing headers with a {@link List} of {@link Header}.
*
* @param values A {@link List} of HTTP headers.
* @return This {@link Builder} instance.
*/
public Builder setHeaders(final List values) {
_headers = new ArrayList<>(values);
return this;
}
@NotNull
private URI _uri;
@NotNull
private List _headers = new ArrayList<>();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy