Problem.java
package de.turnertech.problemdetails;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.InvalidParameterException;
import java.util.Locale;
import java.util.Objects;
import java.util.ResourceBundle;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
/**
* A POJO representation of the urn:ietf:rfc:7807 problem type.
*/
public class Problem {
/** urn:ietf:rfc:7807 */
public static final String NAMESPACE = "urn:ietf:rfc:7807";
/** application/problem+xml */
public static final String MEDIA_TYPE_XML = "application/problem+xml";
/** application/problem+json */
public static final String MEDIA_TYPE_JSON = "application/problem+json";
private static final ResourceBundle i18n = ResourceBundle.getBundle("de.turnertech.problemdetails.i18n");
private static final String PROBLEM_STRING = "problem";
// Mandatory with default
private URI type;
// Optional, must be same as HTTP response if present
private Integer status;
// Advisory
private String title;
// Optional
private String detail;
// Optional
private URI instance;
/**
* Constructs an empty Problem with the type "about:blank" (RFC 9457 - 4.2.1.).
*/
public Problem() {
this(URI.create("about:blank"));
}
/**
* Constructs a Problem with the provided parameters. Note that type may not be null.
* @param type RFC 9457 - 3.1.1.
* @throws NullPointerException if type is null.
*/
public Problem(URI type) {
this(type, null);
}
/**
* Constructs a Problem with the provided parameters. Note that type may not be null.
* @param type RFC 9457 - 3.1.1.
* @param instance RFC 9457 - 3.1.5.
* @throws NullPointerException if type is null.
*/
public Problem(URI type, URI instance) throws NullPointerException {
this.type = Objects.requireNonNull(type, i18n.getString("error.type.nonnull"));
this.instance = instance;
}
/**
* Constructs a deep copy of the provided problem. If you extend this class, make sure
* that your implementation of the copy constructor also returns deep copies.
* @param other problem to copy.
*/
public Problem(Problem other) {
this.type = other.type;
this.status = other.status;
this.title = other.title;
this.detail = other.detail;
this.instance = other.instance;
}
/**
* Gets the problem type.
* @return the problem type.
*/
public URI getType() {
return type;
}
/**
* Sets the problem type.
* @param type the problem type.
* @throws NullPointerException if type is null
*/
public void setType(URI type) throws NullPointerException {
this.type = Objects.requireNonNull(type, i18n.getString("error.type.nonnull"));
}
/**
* Gets the problem status code (HTTP Status Code).
* @return the problem status code (HTTP Status Code)
*/
public Integer getStatus() {
return status;
}
/**
* Sets the problem status code (HTTP Status Code). If set, this must be the same as the one used in your HTTP Response.
* @param status the problem status code (HTTP Status Code).
* @see #findStatusPhrase(int)
*/
public void setStatus(Integer status) {
if(status != null && (status < 100 || status > 599)) {
throw new InvalidParameterException(i18n.getString("error.status.inrange"));
}
this.status = status;
}
/**
* Gets the problem title.
* @return the problem title.
*/
public String getTitle() {
return title;
}
/**
* Sets the problem title.
* @param title the short human readable title.
* @see #findStatusPhrase(int)
*/
public void setTitle(String title) {
this.title = title;
}
/**
* Gets the problem detail.
* @return the problem detail.
*/
public String getDetail() {
return detail;
}
/**
* Sets the problem detail.
* @param detail the problem detail.
*/
public void setDetail(String detail) {
this.detail = detail;
}
/**
* Gets the problem instance.
* @return the problem instance.
*/
public URI getInstance() {
return instance;
}
/**
* Sets the problem instance.
* @param instance the problem instance.
*/
public void setInstance(URI instance) {
this.instance = instance;
}
/**
* Helper for retrieving the HTTP Status Phrase for a HTTP Status Code.
* @param statusCode the HTTP Status Code.
* @return the HTTP Status Phrase in English, may be null.
* @see #findStatusPhrase(int, Locale)
*/
public static String findStatusPhrase(int statusCode) {
return findStatusPhrase(statusCode, Locale.ENGLISH);
}
/**
* Helper for retrieving the localised HTTP Status Phrase for a HTTP Status Code.
* @param statusCode the HTTP Status Code.
* @param locale the desired language.
* @return the HTTP Status Phrase, may be null.
* @see #findStatusPhrase(int)
*/
public static String findStatusPhrase(int statusCode, Locale locale) {
ResourceBundle strings = ResourceBundle.getBundle("de.turnertech.problemdetails.i18n", locale);
return strings.getString(Integer.toString(statusCode));
}
/**
* Converts the contents to a String containing the JSON representation of this Problem.
* @return the JSON String.
* @throws IOException if there are problems with extending the JSON.
* @see #extendJson(OutputStream, Charset)
* @see #toJson(OutputStream)
* @see #toJson(OutputStream, Charset)
*/
public String toJson() throws IOException {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
toJson(outputStream);
return outputStream.toString(StandardCharsets.UTF_8);
}
}
/**
* Writes the Problem to the supplied stream in JSON format using UTF-8.
* @param outputStream the stream to write to.
* @throws IOException if there are problems with extending the JSON.
* @see #extendJson(OutputStream, Charset)
* @see #toJson()
* @see #toJson(OutputStream, Charset)
*/
public void toJson(OutputStream outputStream) throws IOException {
toJson(outputStream, StandardCharsets.UTF_8);
}
/**
* Writes the Problem to the supplied stream in JSON format using the supplied Charset.
* @param outputStream the stream to write to.
* @param charset the Charset to use.
* @throws IOException if there are problems with extending the JSON.
*/
public void toJson(OutputStream outputStream, Charset charset) throws IOException {
try(PrintWriter printWriter = new PrintWriter(outputStream)) {
printWriter.write("{\"type\":\"");
printWriter.write(type.toString());
printWriter.write("\"");
if(title != null) {
printWriter.write(",\"title\":\"");
printWriter.write(title);
printWriter.write("\"");
}
if(status != null) {
printWriter.write(",\"status\":\"");
printWriter.write(Integer.toString(status));
printWriter.write("\"");
}
if(detail != null) {
printWriter.write(",\"detail\":\"");
printWriter.write(detail);
printWriter.write("\"");
}
if(instance != null) {
printWriter.write(",\"instance\":\"");
printWriter.write(instance.toString());
printWriter.write("\"");
}
printWriter.flush();
}
extendJson(outputStream, charset);
outputStream.write("}".getBytes(charset));
}
/**
* <p>Override this if you wish to extend the JSON response. This function is called directly before
* closing the problem element. Pay carefull attention to the namespaces!</p>
* @param outputStream the stream to write to
* @param charset the charset this XML is being written in
* @return true if an element was inserted (so that the caller can decide whether or not to write a ",")
*/
protected boolean extendJson(OutputStream outputStream, Charset charset) {
// Called before closing the last element.
return false;
}
/**
* Converts the contents to a String containing the XML representation of this Problem.
* @return the XML String.
* @throws XMLStreamException if there are problems with extending the XML.
* @see #extendXml(XMLStreamWriter, Charset)
* @see #toXml(OutputStream, Charset, boolean)
* @see #toXml(XMLStreamWriter, Charset, boolean)
*/
public String toXml() throws XMLStreamException {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
toXml(outputStream, StandardCharsets.UTF_8, true);
return outputStream.toString(StandardCharsets.UTF_8);
} catch (IOException e) {
return null;
}
}
/**
* Writes the Problem to the supplied stream in XML format using the supplied charset.
* @param outputStream to write to.
* @param charset to write using.
* @param writeStartDocument to indicate if the xml start document should also be written.
* @throws XMLStreamException if there are problems with extending the XML.
*/
public void toXml(OutputStream outputStream, Charset charset, boolean writeStartDocument) throws XMLStreamException {
XMLOutputFactory outputFactory = XMLOutputFactory.newFactory();
outputFactory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
XMLStreamWriter xmlStreamWriter = outputFactory.createXMLStreamWriter(outputStream, charset.toString());
toXml(xmlStreamWriter, charset, writeStartDocument);
}
/**
* Writes the Problem to the supplied writer in XML format using the supplied charset.
* @param xmlStreamWriter to write using.
* @param charset to write using.
* @param writeStartDocument to indicate if the xml start document should also be written.
* @throws XMLStreamException if there are problems with extending the XML.
*/
public void toXml(XMLStreamWriter xmlStreamWriter, Charset charset, boolean writeStartDocument) throws XMLStreamException {
if(writeStartDocument) {
xmlStreamWriter.writeStartDocument(charset.toString(), "1.0");
}
String defaultNs = xmlStreamWriter.getNamespaceContext().getNamespaceURI("");
String prefix = xmlStreamWriter.getNamespaceContext().getPrefix(NAMESPACE);
if(prefix != null) {
xmlStreamWriter.writeStartElement(NAMESPACE, PROBLEM_STRING);
} else if(defaultNs == null) {
xmlStreamWriter.writeStartElement(PROBLEM_STRING);
xmlStreamWriter.writeDefaultNamespace(NAMESPACE);
} else {
prefix = "p";
int i = 0;
while(xmlStreamWriter.getNamespaceContext().getNamespaceURI(prefix) != null) {
prefix = "p" + Integer.toString(i);
}
xmlStreamWriter.writeStartElement(prefix, PROBLEM_STRING, NAMESPACE);
xmlStreamWriter.writeNamespace(prefix, NAMESPACE);
}
if(type != null) {
xmlStreamWriter.writeStartElement(NAMESPACE, PROBLEM_STRING);
xmlStreamWriter.writeCharacters(type.toString());
xmlStreamWriter.writeEndElement();
}
if(title != null) {
xmlStreamWriter.writeStartElement(NAMESPACE, "title");
xmlStreamWriter.writeCharacters(title);
xmlStreamWriter.writeEndElement();
}
if(status != null) {
xmlStreamWriter.writeStartElement(NAMESPACE, "status");
xmlStreamWriter.writeCharacters(Integer.toString(status));
xmlStreamWriter.writeEndElement();
}
if(detail != null) {
xmlStreamWriter.writeStartElement(NAMESPACE, "detail");
xmlStreamWriter.writeCharacters(detail);
xmlStreamWriter.writeEndElement();
}
if(instance != null) {
xmlStreamWriter.writeStartElement(NAMESPACE, "instance");
xmlStreamWriter.writeCharacters(instance.toString());
xmlStreamWriter.writeEndElement();
}
extendXml(xmlStreamWriter, charset);
// /problem
xmlStreamWriter.writeEndElement();
if(writeStartDocument) {
xmlStreamWriter.writeEndDocument();
}
}
/**
* Override this if you wish to extend the XML response. This function is called directly before
* closing the problem element. Pay carefull attention to the namespaces!
* @param xmlStreamWriter the writer you must use to write to the stream
* @param charset the charset this XML is being written in
* @throws XMLStreamException if there are problems with extending the XML.
*/
protected void extendXml(XMLStreamWriter xmlStreamWriter, Charset charset) throws XMLStreamException {
// Called before closing the last element.
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((type == null) ? 0 : type.hashCode());
result = prime * result + ((status == null) ? 0 : status.hashCode());
result = prime * result + ((title == null) ? 0 : title.hashCode());
result = prime * result + ((detail == null) ? 0 : detail.hashCode());
result = prime * result + ((instance == null) ? 0 : instance.hashCode());
return result;
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Problem other = (Problem) obj;
if (type == null) {
if (other.type != null)
return false;
} else if (!type.equals(other.type))
return false;
if (status == null) {
if (other.status != null)
return false;
} else if (!status.equals(other.status))
return false;
if (title == null) {
if (other.title != null)
return false;
} else if (!title.equals(other.title))
return false;
if (detail == null) {
if (other.detail != null)
return false;
} else if (!detail.equals(other.detail))
return false;
if (instance == null) {
if (other.instance != null)
return false;
} else if (!instance.equals(other.instance))
return false;
return true;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return "Problem [type=" + type + ", title=" + title + "]";
}
}