/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.jooby;
import static java.util.Objects.requireNonNull;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
/**
* Utility class for HTTP responses. Usually you start with a result {@link Results builder} and
* then you customize (or not) one or more HTTP attribute.
*
* <p>
* The following examples build the same output:
* </p>
*
* <pre>
* {
* get("/", (req, rsp) {@literal ->} {
* rsp.status(200).send("Something");
* });
*
* get("/", req {@literal ->} Results.with("Something", 200);
* }
* </pre>
*
* A result is also responsible for content negotiation (if required):
*
* <pre>
* {
* get("/", () {@literal ->} {
* Object model = ...;
* return Results
* .when("text/html", () {@literal ->} Results.html("view").put("model", model))
* .when("application/json", () {@literal ->} model);
* });
* }
* </pre>
*
* <p>
* The example above will render a view when accept header is "text/html" or just send a text
* version of model when the accept header is "application/json".
* </p>
*
* @author edgar
* @since 0.5.0
* @see Results
*/
public class Result {
private static Map<String, Object> NO_HEADERS = ImmutableMap.of();
/** Response headers. */
private Map<String, Object> headers = NO_HEADERS;
/** Response status. */
private Status status;
/** Response content-type. */
private MediaType type;
private final Map<MediaType, Supplier<Object>> data = new LinkedHashMap<>();
/** Quick access to first result . */
private Supplier<Object> first;
/**
* Set response status.
*
* @param status A new response status to use.
* @return This content.
*/
public Result status(final Status status) {
this.status = requireNonNull(status, "A status is required.");
return this;
}
/**
* Set response status.
*
* @param status A new response status to use.
* @return This content.
*/
public Result status(final int status) {
return status(Status.valueOf(status));
}
/**
* Set the content type of this content.
*
* @param type A content type.
* @return This content.
*/
public Result type(final MediaType type) {
this.type = requireNonNull(type, "A content type is required.");
return this;
}
/**
* Set the content type of this content.
*
* @param type A content type.
* @return This content.
*/
public Result type(final String type) {
return type(MediaType.valueOf(type));
}
/**
* Set result content.
*
* @param content A result content.
* @return This content.
*/
public Result set(final Object content) {
Supplier<Object> supplier = () -> content;
first = supplier;
data.put(MediaType.all, supplier);
return this;
}
/**
* Add a when clause for a custom result for the given media-type.
*
* @param type A media type to test for.
* @param supplier An object supplier.
* @return This result.
*/
public Result when(final String type, final Supplier<Object> supplier) {
return when(MediaType.valueOf(type), supplier);
}
/**
* Add a when clause for a custom result for the given media-type.
*
* @param type A media type to test for.
* @param supplier An object supplier.
* @return This result.
*/
public Result when(final MediaType type, final Supplier<Object> supplier) {
requireNonNull(type, "A media type is required.");
requireNonNull(supplier, "A supplier fn is required.");
first = supplier;
data.put(type, supplier);
return this;
}
/**
* @return headers for content.
*/
public Map<String, Object> headers() {
return headers;
}
/**
* @return Body status.
*/
public Optional<Status> status() {
return Optional.ofNullable(status);
}
/**
* @return Body type.
*/
public Optional<MediaType> type() {
return Optional.ofNullable(type);
}
/**
* Get a result value.
*
* @return Value or <code>empty</code>
*/
public Optional<Object> ifGet() {
return ifGet(MediaType.ALL);
}
/**
* Get a result value.
*
* @param <T> Value type.
* @return Value or <code>null</code>
*/
public <T> T get() {
return get(MediaType.ALL);
}
/**
* Get a result value for the given types (accept header).
*
* @param types Accept header.
* @return Result content.
*/
public Optional<Object> ifGet(final List<MediaType> types) {
return Optional.ofNullable(get(types));
}
/**
* Get a result value for the given types (accept header).
*
* @param types Accept header.
* @param <T> Value type.
* @return Result content or <code>null</code>.
*/
@SuppressWarnings("unchecked")
public <T> T get(final List<MediaType> types) {
int size = data.size();
if (size == 1) {
return (T) first.get();
}
if (size == 0) {
return null;
}
Supplier<Object> provider = MediaType
.matcher(types)
.first(ImmutableList.copyOf(data.keySet()))
.map(it -> data.remove(it))
.orElseThrow(
() -> new Err(Status.NOT_ACCEPTABLE, Joiner.on(", ").join(types)));
return (T) provider.get();
}
/**
* Sets a response header with the given name and value. If the header had already been set,
* the new value overwrites the previous one.
*
* @param name Header's name.
* @param value Header's value.
* @return This content.
*/
public Result header(final String name, final Object value) {
requireNonNull(name, "Header's name is required.");
requireNonNull(value, "Header's value is required.");
put(name, value);
return this;
}
/**
* Sets a response header with the given name and value. If the header had already been set,
* the new value overwrites the previous one.
*
* @param name Header's name.
* @param values Header's values.
* @return This content.
*/
public Result header(final String name, final Object... values) {
requireNonNull(name, "Header's name is required.");
requireNonNull(values, "Header's values are required.");
return header(name, ImmutableList.copyOf(values));
}
/**
* Sets a response header with the given name and value. If the header had already been set,
* the new value overwrites the previous one.
*
* @param name Header's name.
* @param values Header's values.
* @return This content.
*/
public Result header(final String name, final Iterable<Object> values) {
requireNonNull(name, "Header's name is required.");
requireNonNull(values, "Header's values are required.");
put(name, values);
return this;
}
@Override
protected Result clone() {
Result result = new Result();
headers.forEach(result::header);
result.status = status;
result.type = type;
result.first = first;
result.data.putAll(data);
return result;
}
private void put(final String name, final Object val) {
headers = ImmutableMap.<String, Object> builder()
.putAll(headers)
.put(name, val)
.build();
}
}