/*
* 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.addthis.hydra.data.filter.value;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import com.addthis.basis.net.HttpUtil;
import com.addthis.basis.net.http.HttpResponse;
import com.addthis.codec.annotations.Time;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* This {@link AbstractValueFilter ValueFilter} <span class="hydra-summary">fetches at most one url</span>.
*
* <p>The value filter accepts an url as input and returns the contents of the url as output.
* This value filter assumes all invocations will provide the same url as input. If a different input
* is provided then an exception is thrown.</p>
*
* @user-reference
*/
public class ValueFilterFetchOnce extends StringFilter {
/**
* Number of milliseconds to wait for response.
* Default is 30 seconds.
*/
private final int timeout;
@JsonCreator
public ValueFilterFetchOnce(@JsonProperty(value = "timeout", required = true)
@Time(TimeUnit.MILLISECONDS) int timeout) {
this.timeout = timeout;
}
private static final AtomicReferenceFieldUpdater<ValueFilterFetchOnce, String> CACHE_UPDATER =
AtomicReferenceFieldUpdater.newUpdater(ValueFilterFetchOnce.class, String.class, "cache");
private volatile String cache;
private CompletableFuture<String> result = new CompletableFuture<>();
@Override public String filter(String input) {
Preconditions.checkNotNull(input, "input to fetch-once filter must be non-null");
while (true) {
String cacheRead = cache;
if (cacheRead == null) {
if (CACHE_UPDATER.compareAndSet(this, null, input)) {
makeHttpRequest(input);
}
} else if (!cacheRead.equals(input)) {
throw new IllegalStateException("fetch attempted on multiple urls " +
cacheRead + " and " + input);
} else {
try {
return result.get(timeout, TimeUnit.MILLISECONDS);
} catch (ExecutionException ex) {
throw Throwables.propagate(ex.getCause());
} catch (Exception ex) {
throw Throwables.propagate(ex);
}
}
}
}
private void makeHttpRequest(String input) {
try {
HttpResponse response = HttpUtil.httpGet(input, timeout);
int status = response.getStatus();
byte[] body = response.getBody();
if ((status == 200) && (body != null)) {
result.complete(new String(body));
} if ((status == 200) && (body == null)) {
result.completeExceptionally(new IOException("Received empty response"));
} else {
String message = "Received non-200 status code from request: " + status;
if (body != null) {
message += " with response body " + new String(body);
} else {
message += " with no response body";
}
result.completeExceptionally(new IOException(message));
}
} catch (Exception ex) {
result.completeExceptionally(ex);
}
}
}