/*
* Copyright 2016 the original author or authors.
*
* 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.springframework.data.rest.webmvc;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.time.Instant;
import java.util.Date;
import java.util.Optional;
import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.auditing.AuditableBeanWrapper;
import org.springframework.data.auditing.AuditableBeanWrapperFactory;
import org.springframework.data.convert.Jsr310Converters;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.rest.webmvc.support.ETag;
import org.springframework.http.HttpHeaders;
import org.springframework.util.Assert;
/**
* Value object to prepare {@link HttpHeaders} for {@link PersistentEntityResource} and {@link PersistentEntity}
* instances.
*
* @author Oliver Gierke
* @soundtrack Ron Spielman Trio - Matchstick
*/
@RequiredArgsConstructor
public class HttpHeadersPreparer {
private final @NonNull AuditableBeanWrapperFactory auditableBeanWrapperFactory;
private final ConfigurableConversionService conversionService = new DefaultConversionService();
{
Jsr310Converters.getConvertersToRegister().forEach(conversionService::addConverter);
}
/**
* Returns the default headers to be returned for the given {@link PersistentEntityResource}. Will set {@link ETag}
* and {@code Last-Modified} headers if applicable.
*
* @param resource can be {@literal null}.
* @return
*/
public HttpHeaders prepareHeaders(Optional<PersistentEntityResource> resource) {
return resource//
.map(it -> prepareHeaders(it.getPersistentEntity(), it.getContent()))//
.orElseGet(() -> new HttpHeaders());
}
/**
* Returns the default headers to be returned for the given {@link PersistentEntity} and value. Will set {@link ETag}
* and {@code Last-Modified} headers if applicable.
*
* @param entity must not be {@literal null}.
* @param value must not be {@literal null}.
* @return
*/
public HttpHeaders prepareHeaders(PersistentEntity<?, ?> entity, Object value) {
// Add ETag
HttpHeaders headers = ETag.from(entity, value).addTo(new HttpHeaders());
// Add Last-Modified
getLastModifiedInMilliseconds(value).ifPresent(it -> headers.setLastModified(it));
return headers;
}
/**
* Returns whether the given object is still valid in the context of the given {@link HttpHeaders}' requirements.
*
* @param source must not be {@literal null}.
* @param headers must not be {@literal null}.
* @return
*/
public boolean isObjectStillValid(Object source, HttpHeaders headers) {
Assert.notNull(source, "Source object must not be null!");
Assert.notNull(headers, "HttpHeaders must not be null!");
if (headers.getIfModifiedSince() == -1) {
return false;
}
return getLastModifiedInMilliseconds(source)//
.map(it -> it / 1000 * 1000 <= headers.getIfModifiedSince())//
.orElse(true);
}
/**
* Returns the {@link AuditableBeanWrapper} for the given source.
*
* @param source can be {@literal null}.
* @return
*/
private Optional<AuditableBeanWrapper> getAuditableBeanWrapper(Object source) {
return auditableBeanWrapperFactory.getBeanWrapperFor(source);
}
private Optional<Long> getLastModifiedInMilliseconds(Object object) {
return getAuditableBeanWrapper(object)//
.flatMap(it -> it.getLastModifiedDate())//
.map(it -> conversionService.convert(it, Date.class))//
.map(it -> conversionService.convert(it, Instant.class))//
.map(it -> it.toEpochMilli());
}
}