Problem.java

  1. package de.turnertech.problemdetails;

  2. import java.io.ByteArrayOutputStream;
  3. import java.io.IOException;
  4. import java.io.OutputStream;
  5. import java.io.PrintWriter;
  6. import java.net.URI;
  7. import java.nio.charset.Charset;
  8. import java.nio.charset.StandardCharsets;
  9. import java.security.InvalidParameterException;
  10. import java.util.Locale;
  11. import java.util.Objects;
  12. import java.util.ResourceBundle;

  13. import javax.xml.stream.XMLOutputFactory;
  14. import javax.xml.stream.XMLStreamException;
  15. import javax.xml.stream.XMLStreamWriter;

  16. /**
  17.  * A POJO representation of the urn:ietf:rfc:7807 problem type.
  18.  */
  19. public class Problem {
  20.    
  21.     /** urn:ietf:rfc:7807 */
  22.     public static final String NAMESPACE = "urn:ietf:rfc:7807";
  23.    
  24.     /** application/problem+xml */
  25.     public static final String MEDIA_TYPE_XML = "application/problem+xml";
  26.    
  27.     /** application/problem+json */
  28.     public static final String MEDIA_TYPE_JSON = "application/problem+json";
  29.    
  30.     private static final ResourceBundle i18n = ResourceBundle.getBundle("de.turnertech.problemdetails.i18n");

  31.     private static final String PROBLEM_STRING = "problem";
  32.    
  33.     // Mandatory with default
  34.     private URI type;
  35.    
  36.     // Optional, must be same as HTTP response if present
  37.     private Integer status;
  38.    
  39.     // Advisory
  40.     private String title;
  41.    
  42.     // Optional
  43.     private String detail;
  44.    
  45.     // Optional
  46.     private URI instance;

  47.     /**
  48.      * Constructs an empty Problem with the type "about:blank" (RFC 9457 - 4.2.1.).
  49.      */
  50.     public Problem() {
  51.         this(URI.create("about:blank"));
  52.     }

  53.     /**
  54.      * Constructs a Problem with the provided parameters. Note that type may not be null.
  55.      * @param type RFC 9457 - 3.1.1.
  56.      * @throws NullPointerException if type is null.
  57.      */
  58.     public Problem(URI type) {
  59.         this(type, null);
  60.     }

  61.     /**
  62.      * Constructs a Problem with the provided parameters. Note that type may not be null.
  63.      * @param type RFC 9457 - 3.1.1.
  64.      * @param instance RFC 9457 - 3.1.5.
  65.      * @throws NullPointerException if type is null.
  66.      */
  67.     public Problem(URI type, URI instance) throws NullPointerException {
  68.         this.type = Objects.requireNonNull(type, i18n.getString("error.type.nonnull"));
  69.         this.instance = instance;
  70.     }
  71.    
  72.     /**
  73.      * Constructs a deep copy of the provided problem. If you extend this class, make sure
  74.      * that your implementation of the copy constructor also returns deep copies.
  75.      * @param other problem to copy.
  76.      */
  77.     public Problem(Problem other) {
  78.         this.type = other.type;
  79.         this.status = other.status;
  80.         this.title = other.title;
  81.         this.detail = other.detail;
  82.         this.instance = other.instance;
  83.     }

  84.     /**
  85.      * Gets the problem type.
  86.      * @return the problem type.
  87.      */
  88.     public URI getType() {
  89.         return type;
  90.     }

  91.     /**
  92.      * Sets the problem type.
  93.      * @param type the problem type.
  94.      * @throws NullPointerException if type is null
  95.      */
  96.     public void setType(URI type) throws NullPointerException {
  97.         this.type = Objects.requireNonNull(type, i18n.getString("error.type.nonnull"));
  98.     }

  99.     /**
  100.      * Gets the problem status code (HTTP Status Code).
  101.      * @return the problem status code (HTTP Status Code)
  102.      */
  103.     public Integer getStatus() {
  104.         return status;
  105.     }

  106.     /**
  107.      * Sets the problem status code (HTTP Status Code). If set, this must be the same as the one used in your HTTP Response.
  108.      * @param status the problem status code (HTTP Status Code).
  109.      * @see #findStatusPhrase(int)
  110.      */
  111.     public void setStatus(Integer status) {
  112.         if(status != null && (status < 100 || status > 599)) {
  113.             throw new InvalidParameterException(i18n.getString("error.status.inrange"));
  114.         }
  115.         this.status = status;
  116.     }

  117.     /**
  118.      * Gets the problem title.
  119.      * @return the problem title.
  120.      */
  121.     public String getTitle() {
  122.         return title;
  123.     }

  124.     /**
  125.      * Sets the problem title.
  126.      * @param title the short human readable title.
  127.      * @see #findStatusPhrase(int)
  128.      */
  129.     public void setTitle(String title) {
  130.         this.title = title;
  131.     }

  132.     /**
  133.      * Gets the problem detail.
  134.      * @return the problem detail.
  135.      */
  136.     public String getDetail() {
  137.         return detail;
  138.     }

  139.     /**
  140.      * Sets the problem detail.
  141.      * @param detail the problem detail.
  142.      */
  143.     public void setDetail(String detail) {
  144.         this.detail = detail;
  145.     }

  146.     /**
  147.      * Gets the problem instance.
  148.      * @return the problem instance.
  149.      */
  150.     public URI getInstance() {
  151.         return instance;
  152.     }

  153.     /**
  154.      * Sets the problem instance.
  155.      * @param instance the problem instance.
  156.      */
  157.     public void setInstance(URI instance) {
  158.         this.instance = instance;
  159.     }

  160.     /**
  161.      * Helper for retrieving the HTTP Status Phrase for a HTTP Status Code.
  162.      * @param statusCode the HTTP Status Code.
  163.      * @return the HTTP Status Phrase in English, may be null.
  164.      * @see #findStatusPhrase(int, Locale)
  165.      */
  166.     public static String findStatusPhrase(int statusCode) {
  167.         return findStatusPhrase(statusCode, Locale.ENGLISH);
  168.     }

  169.     /**
  170.      * Helper for retrieving the localised HTTP Status Phrase for a HTTP Status Code.
  171.      * @param statusCode the HTTP Status Code.
  172.      * @param locale the desired language.
  173.      * @return the HTTP Status Phrase, may be null.
  174.      * @see #findStatusPhrase(int)
  175.      */
  176.     public static String findStatusPhrase(int statusCode, Locale locale) {
  177.         ResourceBundle strings = ResourceBundle.getBundle("de.turnertech.problemdetails.i18n", locale);
  178.         return strings.getString(Integer.toString(statusCode));
  179.     }
  180.    
  181.     /**
  182.      * Converts the contents to a String containing the JSON representation of this Problem.
  183.      * @return the JSON String.
  184.      * @throws IOException if there are problems with extending the JSON.
  185.      * @see #extendJson(OutputStream, Charset)
  186.      * @see #toJson(OutputStream)
  187.      * @see #toJson(OutputStream, Charset)
  188.      */
  189.     public String toJson() throws IOException {
  190.         try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
  191.             toJson(outputStream);
  192.             return outputStream.toString(StandardCharsets.UTF_8);
  193.         }
  194.     }

  195.     /**
  196.      * Writes the Problem to the supplied stream in JSON format using UTF-8.
  197.      * @param outputStream the stream to write to.
  198.      * @throws IOException if there are problems with extending the JSON.
  199.      * @see #extendJson(OutputStream, Charset)
  200.      * @see #toJson()
  201.      * @see #toJson(OutputStream, Charset)
  202.      */
  203.     public void toJson(OutputStream outputStream) throws IOException {
  204.         toJson(outputStream, StandardCharsets.UTF_8);
  205.     }

  206.     /**
  207.      * Writes the Problem to the supplied stream in JSON format using the supplied Charset.
  208.      * @param outputStream the stream to write to.
  209.      * @param charset the Charset to use.
  210.      * @throws IOException if there are problems with extending the JSON.
  211.      */
  212.     public void toJson(OutputStream outputStream, Charset charset) throws IOException {
  213.         try(PrintWriter printWriter = new PrintWriter(outputStream)) {
  214.             printWriter.write("{\"type\":\"");
  215.             printWriter.write(type.toString());
  216.             printWriter.write("\"");

  217.             if(title != null) {
  218.                 printWriter.write(",\"title\":\"");
  219.                 printWriter.write(title);
  220.                 printWriter.write("\"");
  221.             }
  222.             if(status != null) {
  223.                 printWriter.write(",\"status\":\"");
  224.                 printWriter.write(Integer.toString(status));
  225.                 printWriter.write("\"");
  226.             }
  227.             if(detail != null) {
  228.                 printWriter.write(",\"detail\":\"");
  229.                 printWriter.write(detail);
  230.                 printWriter.write("\"");
  231.             }
  232.             if(instance != null) {
  233.                 printWriter.write(",\"instance\":\"");
  234.                 printWriter.write(instance.toString());
  235.                 printWriter.write("\"");
  236.             }

  237.             printWriter.flush();

  238.         }

  239.         extendJson(outputStream, charset);

  240.         outputStream.write("}".getBytes(charset));
  241.     }

  242.     /**
  243.      * <p>Override this if you wish to extend the JSON response. This function is called directly before
  244.      * closing the problem element. Pay carefull attention to the namespaces!</p>
  245.      * @param outputStream the stream to write to
  246.      * @param charset the charset this XML is being written in
  247.      * @return true if an element was inserted (so that the caller can decide whether or not to write a ",")
  248.      */
  249.     protected boolean extendJson(OutputStream outputStream, Charset charset) {
  250.         // Called before closing the last element.
  251.         return false;
  252.     }

  253.     /**
  254.      * Converts the contents to a String containing the XML representation of this Problem.
  255.      * @return the XML String.
  256.      * @throws XMLStreamException if there are problems with extending the XML.
  257.      * @see #extendXml(XMLStreamWriter, Charset)
  258.      * @see #toXml(OutputStream, Charset, boolean)
  259.      * @see #toXml(XMLStreamWriter, Charset, boolean)
  260.      */
  261.     public String toXml() throws XMLStreamException {
  262.         try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
  263.             toXml(outputStream, StandardCharsets.UTF_8, true);
  264.             return outputStream.toString(StandardCharsets.UTF_8);
  265.         } catch (IOException e) {
  266.             return null;
  267.         }
  268.     }
  269.    
  270.     /**
  271.      * Writes the Problem to the supplied stream in XML format using the supplied charset.
  272.      * @param outputStream to write to.
  273.      * @param charset to write using.
  274.      * @param writeStartDocument to indicate if the xml start document should also be written.
  275.      * @throws XMLStreamException if there are problems with extending the XML.
  276.      */
  277.     public void toXml(OutputStream outputStream, Charset charset, boolean writeStartDocument) throws XMLStreamException {
  278.         XMLOutputFactory outputFactory = XMLOutputFactory.newFactory();
  279.         outputFactory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
  280.         XMLStreamWriter xmlStreamWriter = outputFactory.createXMLStreamWriter(outputStream, charset.toString());
  281.         toXml(xmlStreamWriter, charset, writeStartDocument);
  282.     }
  283.    
  284.     /**
  285.      * Writes the Problem to the supplied writer in XML format using the supplied charset.
  286.      * @param xmlStreamWriter to write using.
  287.      * @param charset to write using.
  288.      * @param writeStartDocument to indicate if the xml start document should also be written.
  289.      * @throws XMLStreamException if there are problems with extending the XML.
  290.      */
  291.     public void toXml(XMLStreamWriter xmlStreamWriter, Charset charset, boolean writeStartDocument) throws XMLStreamException {
  292.         if(writeStartDocument) {
  293.             xmlStreamWriter.writeStartDocument(charset.toString(), "1.0");
  294.         }
  295.        
  296.         String defaultNs = xmlStreamWriter.getNamespaceContext().getNamespaceURI("");
  297.         String prefix = xmlStreamWriter.getNamespaceContext().getPrefix(NAMESPACE);
  298.        
  299.         if(prefix != null) {
  300.             xmlStreamWriter.writeStartElement(NAMESPACE, PROBLEM_STRING);
  301.         } else if(defaultNs == null) {
  302.             xmlStreamWriter.writeStartElement(PROBLEM_STRING);
  303.             xmlStreamWriter.writeDefaultNamespace(NAMESPACE);
  304.         } else {
  305.             prefix = "p";
  306.             int i = 0;
  307.             while(xmlStreamWriter.getNamespaceContext().getNamespaceURI(prefix) != null) {
  308.                 prefix = "p" + Integer.toString(i);
  309.             }
  310.             xmlStreamWriter.writeStartElement(prefix, PROBLEM_STRING, NAMESPACE);
  311.             xmlStreamWriter.writeNamespace(prefix, NAMESPACE);
  312.         }
  313.        
  314.         if(type != null) {
  315.             xmlStreamWriter.writeStartElement(NAMESPACE, PROBLEM_STRING);
  316.             xmlStreamWriter.writeCharacters(type.toString());
  317.             xmlStreamWriter.writeEndElement();
  318.         }

  319.         if(title != null) {
  320.             xmlStreamWriter.writeStartElement(NAMESPACE, "title");
  321.             xmlStreamWriter.writeCharacters(title);
  322.             xmlStreamWriter.writeEndElement();
  323.         }

  324.         if(status != null) {
  325.             xmlStreamWriter.writeStartElement(NAMESPACE, "status");
  326.             xmlStreamWriter.writeCharacters(Integer.toString(status));
  327.             xmlStreamWriter.writeEndElement();
  328.         }

  329.         if(detail != null) {
  330.             xmlStreamWriter.writeStartElement(NAMESPACE, "detail");
  331.             xmlStreamWriter.writeCharacters(detail);
  332.             xmlStreamWriter.writeEndElement();
  333.         }

  334.         if(instance != null) {
  335.             xmlStreamWriter.writeStartElement(NAMESPACE, "instance");
  336.             xmlStreamWriter.writeCharacters(instance.toString());
  337.             xmlStreamWriter.writeEndElement();
  338.         }
  339.        
  340.         extendXml(xmlStreamWriter, charset);
  341.        
  342.         // /problem
  343.         xmlStreamWriter.writeEndElement();
  344.        
  345.         if(writeStartDocument) {
  346.             xmlStreamWriter.writeEndDocument();
  347.         }
  348.     }
  349.    
  350.     /**
  351.      * Override this if you wish to extend the XML response. This function is called directly before
  352.      * closing the problem element. Pay carefull attention to the namespaces!
  353.      * @param xmlStreamWriter the writer you must use to write to the stream
  354.      * @param charset the charset this XML is being written in
  355.      * @throws XMLStreamException if there are problems with extending the XML.
  356.      */
  357.     protected void extendXml(XMLStreamWriter xmlStreamWriter, Charset charset) throws XMLStreamException {
  358.         // Called before closing the last element.
  359.     }

  360.     /**
  361.      * {@inheritDoc}
  362.      */
  363.     @Override
  364.     public int hashCode() {
  365.         final int prime = 31;
  366.         int result = 1;
  367.         result = prime * result + ((type == null) ? 0 : type.hashCode());
  368.         result = prime * result + ((status == null) ? 0 : status.hashCode());
  369.         result = prime * result + ((title == null) ? 0 : title.hashCode());
  370.         result = prime * result + ((detail == null) ? 0 : detail.hashCode());
  371.         result = prime * result + ((instance == null) ? 0 : instance.hashCode());
  372.         return result;
  373.     }

  374.     /**
  375.      * {@inheritDoc}
  376.      */
  377.     @Override
  378.     public boolean equals(Object obj) {
  379.         if (this == obj)
  380.             return true;
  381.         if (obj == null)
  382.             return false;
  383.         if (getClass() != obj.getClass())
  384.             return false;
  385.         Problem other = (Problem) obj;
  386.         if (type == null) {
  387.             if (other.type != null)
  388.                 return false;
  389.         } else if (!type.equals(other.type))
  390.             return false;
  391.         if (status == null) {
  392.             if (other.status != null)
  393.                 return false;
  394.         } else if (!status.equals(other.status))
  395.             return false;
  396.         if (title == null) {
  397.             if (other.title != null)
  398.                 return false;
  399.         } else if (!title.equals(other.title))
  400.             return false;
  401.         if (detail == null) {
  402.             if (other.detail != null)
  403.                 return false;
  404.         } else if (!detail.equals(other.detail))
  405.             return false;
  406.         if (instance == null) {
  407.             if (other.instance != null)
  408.                 return false;
  409.         } else if (!instance.equals(other.instance))
  410.             return false;
  411.         return true;
  412.     }

  413.     /**
  414.      * {@inheritDoc}
  415.      */
  416.     @Override
  417.     public String toString() {
  418.         return "Problem [type=" + type + ", title=" + title + "]";
  419.     }
  420.    
  421. }