/*
* Copyright (c) 2008, The Codehaus. All Rights Reserved.
*
* 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 org.codehaus.httpcache4j;
import org.codehaus.httpcache4j.util.OptionalUtils;
import org.codehaus.httpcache4j.util.Preconditions;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
/**
* Represents the different conditional types that an HTTP request may have.
* This are basically 4 things:
* <ul>
* <li>If-Match</li>
* <li>If-None-Match</li>
* <li>If-Unmodified-Since</li>
* <li>If-Modified-Since</li>
* </ul>
*
* <p> Combinations of these conditionals are possible with the following exceptions </p>
*
* <table summary="Usage">
* <thead>
* <tr><th>Conditional</th><th>Can be combined with</th><th>Unspecified</th></tr>
* </thead>
* <tbody>
* <tr>
* <td>If-Match</td><td>If-Unmodified-Since</td><td>If-None-Match, If-Modified-Since</td>
* </tr>
* <tr>
* <td>If-None-Match</td><td>If-Modified-Since</td><td>If-Match, If-Unmodified-Since</td>
* </tr>
* <tr>
* <td>If-Unmodified-Since</td><td>If-Match</td><td>If-None-Match, If-Modified-Since</td>
* </tr>
* <tr>
* <td>If-Modified-Since</td><td>If-None-Match</td><td>If-Match, If-Unmodified-Since</td>
* </tr>
* </tbody>
* </table>
*
* @author <a href="mailto:hamnis@codehaus.org">Erlend Hamnaberg</a>
*/
public final class Conditionals {
private final List<Tag> match;
private final List<Tag> noneMatch;
private final Optional<LocalDateTime> modifiedSince;
private final Optional<LocalDateTime> unModifiedSince;
private static final String ERROR_MESSAGE = "The combination of %s and %s is undefined by the HTTP specification";
public Conditionals() {
this(empty(), empty(), Optional.empty(), Optional.empty());
}
private static List<Tag> empty() {
return Collections.emptyList();
}
public Conditionals(List<Tag> match, List<Tag> noneMatch, Optional<LocalDateTime> modifiedSince, Optional<LocalDateTime> unModifiedSince) {
this.match = match;
this.noneMatch = noneMatch;
this.modifiedSince = modifiedSince;
this.unModifiedSince = unModifiedSince;
}
/**
* Adds tags to the If-Match header.
*
* @param tag the tag to add, may be null. This means the same as adding {@link Tag#ALL}
* @throws IllegalArgumentException if ALL is supplied more than once, or you add a null tag more than once.
* @return a new Conditionals object with the If-Match tag added.
*/
public Conditionals addIfMatch(Tag tag) {
Preconditions.checkArgument(!modifiedSince.isPresent(), String.format(ERROR_MESSAGE, HeaderConstants.IF_MATCH, HeaderConstants.IF_MODIFIED_SINCE));
Preconditions.checkArgument(noneMatch.isEmpty(), String.format(ERROR_MESSAGE, HeaderConstants.IF_MATCH, HeaderConstants.IF_NONE_MATCH));
List<Tag> match = new ArrayList<>(this.match);
if (tag == null) {
tag = Tag.ALL;
}
if (Tag.ALL.equals(tag)) {
match.clear();
}
if (!match.contains(Tag.ALL)) {
if (!match.contains(tag)) {
match.add(tag);
}
}
else {
throw new IllegalArgumentException("Tag ALL already in the list");
}
return new Conditionals(Collections.unmodifiableList(match), empty(), Optional.empty(), unModifiedSince);
}
/**
* Adds tags to the If-None-Match header.
*
* The meaning of "If-None-Match: *" is that the method MUST NOT be performed if the representation selected by
* the origin server (or by a cache, possibly using the Vary mechanism, see section 14.44) exists,
* and SHOULD be performed if the representation does not exist.
* This feature is intended to be useful in preventing races between PUT operations.
*
* @param tag the tag to add, may be null. This means the same as adding {@link Tag#ALL}
* @throws IllegalArgumentException if ALL is supplied more than once, or you add a null tag more than once.
* @return a new Conditionals object with the If-None-Match tag added.
*/
public Conditionals addIfNoneMatch(Tag tag) {
Preconditions.checkArgument(!unModifiedSince.isPresent(), String.format(ERROR_MESSAGE, HeaderConstants.IF_NONE_MATCH, HeaderConstants.IF_UNMODIFIED_SINCE));
Preconditions.checkArgument(match.isEmpty(), String.format(ERROR_MESSAGE, HeaderConstants.IF_NONE_MATCH, HeaderConstants.IF_MATCH));
List<Tag> noneMatch = new ArrayList<>(this.noneMatch);
if (tag == null) {
tag = Tag.ALL;
}
if (Tag.ALL.equals(tag)) {
noneMatch.clear();
}
if (!noneMatch.contains(Tag.ALL)) {
if (!noneMatch.contains(tag)) {
noneMatch.add(tag);
}
}
else {
throw new IllegalArgumentException("Tag ALL already in the list");
}
return new Conditionals(empty(), Collections.unmodifiableList(noneMatch), modifiedSince, Optional.empty());
}
/**
* You should use the server's time here. Otherwise you might get unexpected results.
*
* The typical use case is:
*
*
* <pre>
* HTTPResponse response = ....
* HTTPRequest request = createRequest();
* request = request.conditionals(new Conditionals().ifModifiedSince(response.getLastModified());
* </pre>
*
* @param time the time to check.
* @return the conditionals with the If-Modified-Since date set.
*/
public Conditionals ifModifiedSince(LocalDateTime time) {
Preconditions.checkArgument(match.isEmpty(), String.format(ERROR_MESSAGE, HeaderConstants.IF_MODIFIED_SINCE, HeaderConstants.IF_MATCH));
Preconditions.checkArgument(!unModifiedSince.isPresent(), String.format(ERROR_MESSAGE, HeaderConstants.IF_MODIFIED_SINCE, HeaderConstants.IF_UNMODIFIED_SINCE));
time = time.withNano(0);
return new Conditionals(empty(), noneMatch, Optional.of(time), Optional.empty());
}
/**
* You should use the server's time here. Otherwise you might get unexpected results.
* The typical use case is:
*
*
* <pre>
* HTTPResponse response = ....
* HTTPRequest request = createRequest();
* request = request.conditionals(new Conditionals().ifUnModifiedSince(response.getLastModified());
* </pre>
*
* @param time the time to check.
* @return the conditionals with the If-Unmodified-Since date set.
*/
public Conditionals ifUnModifiedSince(LocalDateTime time) {
Preconditions.checkArgument(noneMatch.isEmpty(), String.format(ERROR_MESSAGE, HeaderConstants.IF_UNMODIFIED_SINCE, HeaderConstants.IF_NONE_MATCH));
Preconditions.checkArgument(!modifiedSince.isPresent(), String.format(ERROR_MESSAGE, HeaderConstants.IF_UNMODIFIED_SINCE, HeaderConstants.IF_MODIFIED_SINCE));
time = time.withNano(0);
return new Conditionals(match, empty(), Optional.empty(), Optional.of(time));
}
public List<Tag> getMatch() {
return Collections.unmodifiableList(match);
}
public List<Tag> getNoneMatch() {
return Collections.unmodifiableList(noneMatch);
}
public Optional<LocalDateTime> getModifiedSince() {
return modifiedSince;
}
public Optional<LocalDateTime> getUnModifiedSince() {
return unModifiedSince;
}
/**
*
* @return {@code true} if the Conditionals represents a unconditional request.
*/
public boolean isUnconditional() {
return noneMatch.contains(Tag.ALL) || match.contains(Tag.ALL) || (match.isEmpty() && !unModifiedSince.isPresent()) || (noneMatch.isEmpty() && !modifiedSince.isPresent()) ;
}
/**
* Converts the Conditionals into real headers.
* @return real headers.
*/
public Headers toHeaders() {
Headers headers = new Headers();
if (!getMatch().isEmpty()) {
headers = headers.add(new Header(HeaderConstants.IF_MATCH, buildTagHeaderValue(getMatch())));
}
if (!getNoneMatch().isEmpty()) {
headers = headers.add(new Header(HeaderConstants.IF_NONE_MATCH, buildTagHeaderValue(getNoneMatch())));
}
if (modifiedSince.isPresent()) {
headers = headers.set(HeaderUtils.toHttpDate(HeaderConstants.IF_MODIFIED_SINCE, modifiedSince.get()));
}
if (unModifiedSince.isPresent()) {
headers = headers.set(HeaderUtils.toHttpDate(HeaderConstants.IF_UNMODIFIED_SINCE, unModifiedSince.get()));
}
return headers;
}
public static Conditionals valueOf(Headers headers) {
List<Tag> ifMatch = makeTags(headers.getFirstHeaderValue(HeaderConstants.IF_MATCH).orElse(null));
List<Tag> ifNoneMatch = makeTags(headers.getFirstHeaderValue(HeaderConstants.IF_NONE_MATCH).orElse(null));
Optional<LocalDateTime> modifiedSince = headers.getFirstHeader(HeaderConstants.IF_MODIFIED_SINCE).flatMap(HeaderUtils::fromHttpDate);
Optional<LocalDateTime> unModifiedSince = headers.getFirstHeader(HeaderConstants.IF_UNMODIFIED_SINCE).flatMap(HeaderUtils::fromHttpDate);
return new Conditionals(ifMatch, ifNoneMatch, modifiedSince, unModifiedSince);
}
private static List<Tag> makeTags(String ifMatch) {
if (ifMatch == null) {
return Arrays.asList();
}
return Collections.unmodifiableList(Arrays.asList(ifMatch.split(",")).stream().
filter(m -> !Objects.toString(m, "").isEmpty()).
map(String::trim).
flatMap(t -> OptionalUtils.stream(Tag.parse(t))).collect(Collectors.toList()));
}
private String buildTagHeaderValue(List<Tag> match) {
return match.stream().map(Tag::format).collect(Collectors.joining(","));
}
}