001/*
002 * Copyright © 2025 CUI-OpenSource-Software (info@cuioss.de)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package de.cuioss.http.security.data;
017
018import de.cuioss.http.security.core.ValidationType;
019import org.jspecify.annotations.Nullable;
020
021import java.util.Optional;
022
023/**
024 * Immutable record representing an HTTP request or response body with content, content type, and encoding.
025 *
026 * <p>This record encapsulates the structure of HTTP message bodies, providing a type-safe way
027 * to handle body data in HTTP security validation. It supports various content types and
028 * encoding schemes commonly used in HTTP communications.</p>
029 *
030 * <h3>Design Principles</h3>
031 * <ul>
032 *   <li><strong>Immutability</strong> - All fields are final and the record cannot be modified</li>
033 *   <li><strong>Type Safety</strong> - Strongly typed representation of HTTP body data</li>
034 *   <li><strong>Encoding Awareness</strong> - Explicit handling of content encoding</li>
035 *   <li><strong>Content Type Support</strong> - Supports MIME type specification</li>
036 * </ul>
037 *
038 * <h3>Usage Examples</h3>
039 * <pre>
040 * // JSON body
041 * HTTPBody jsonBody = new HTTPBody(
042 *     "{\"userId\": 123, \"name\": \"John\"}",
043 *     "application/json",
044 *     ""
045 * );
046 *
047 * // Form data
048 * HTTPBody formBody = new HTTPBody(
049 *     "username=admin&amp;password=secret",
050 *     "application/x-www-form-urlencoded",
051 *     ""
052 * );
053 *
054 * // Compressed content
055 * HTTPBody compressedBody = new HTTPBody(
056 *     "...", // compressed content
057 *     "text/html",
058 *     "gzip"
059 * );
060 *
061 * // Access components
062 * String content = body.content();           // The actual content
063 * String contentType = body.contentType();   // "application/json"
064 * String encoding = body.encoding();         // "gzip"
065 *
066 * // Check content characteristics
067 * boolean isJson = body.isJson();           // true for JSON content
068 * boolean hasContent = body.hasContent();   // true if content is not empty
069 * boolean isCompressed = body.isCompressed(); // true if encoding is specified
070 *
071 * // Use in validation
072 * validator.validate(body.content(), ValidationType.BODY);
073 * </pre>
074 *
075 * <h3>Content Types</h3>
076 * <p>The contentType field should contain a valid MIME type (e.g., "application/json",
077 * "text/html", "multipart/form-data"). An empty string indicates no content type is specified.</p>
078 *
079 * <h3>Encoding</h3>
080 * <p>The encoding field specifies content encoding such as "gzip", "deflate", "br" (Brotli),
081 * or "" for no encoding. This is distinct from character encoding, which is typically
082 * specified in the Content-Type header.</p>
083 *
084 * <h3>Security Considerations</h3>
085 * <p>This record is a simple data container. Security validation should be applied to
086 * the content using appropriate validators for {@link ValidationType#BODY}, taking into
087 * account the content type and encoding when determining validation strategies.</p>
088 *
089 * Implements: Task B3 from HTTP verification specification
090 *
091 * @param content The body content as a string
092 * @param contentType The MIME content type (e.g., "application/json", "text/html")
093 * @param encoding The content encoding (e.g., "gzip", "deflate", "" for none)
094 *
095 * @since 1.0
096 * @see ValidationType#BODY
097 */
098public record HTTPBody(@Nullable String content, @Nullable String contentType, @Nullable String encoding) {
099
100    public static HTTPBody of(String content, String contentType) {
101        return new HTTPBody(content, contentType, "");
102    }
103
104    public static HTTPBody text(String content) {
105        return new HTTPBody(content, "text/plain", "");
106    }
107
108    public static HTTPBody json(String jsonContent) {
109        return new HTTPBody(jsonContent, "application/json", "");
110    }
111
112    public static HTTPBody html(String htmlContent) {
113        return new HTTPBody(htmlContent, "text/html", "");
114    }
115
116    public static HTTPBody form(String formContent) {
117        return new HTTPBody(formContent, "application/x-www-form-urlencoded", "");
118    }
119
120    public boolean hasContent() {
121        return content != null && !content.isEmpty();
122    }
123
124    public boolean hasContentType() {
125        return contentType != null && !contentType.isEmpty();
126    }
127
128    public boolean hasEncoding() {
129        return encoding != null && !encoding.isEmpty();
130    }
131
132    public boolean isCompressed() {
133        return hasEncoding();
134    }
135
136    @SuppressWarnings("ConstantConditions")
137    public boolean isJson() {
138        return hasContentType() && contentType.toLowerCase().contains("json");
139    }
140
141    /**
142     * Checks if the content type indicates XML content.
143     *
144     * @return true if the content type contains "xml"
145     */
146    @SuppressWarnings("ConstantConditions")
147    public boolean isXml() {
148        return hasContentType() && contentType.toLowerCase().contains("xml");
149    }
150
151    /**
152     * Checks if the content type indicates HTML content.
153     *
154     * @return true if the content type contains "html"
155     */
156    @SuppressWarnings("ConstantConditions")
157    public boolean isHtml() {
158        return hasContentType() && contentType.toLowerCase().contains("html");
159    }
160
161    /**
162     * Checks if the content type indicates plain text.
163     *
164     * @return true if the content type is "text/plain"
165     */
166    public boolean isPlainText() {
167        return hasContentType() && "text/plain".equalsIgnoreCase(contentType);
168    }
169
170    /**
171     * Checks if the content type indicates form data.
172     *
173     * @return true if the content type is form-encoded
174     */
175    @SuppressWarnings("ConstantConditions")
176    public boolean isFormData() {
177        return hasContentType() &&
178                (contentType.toLowerCase().contains("application/x-www-form-urlencoded") ||
179                        contentType.toLowerCase().contains("multipart/form-data"));
180    }
181
182    /**
183     * Checks if the content type indicates binary content.
184     *
185     * @return true if the content type suggests binary data
186     */
187    @SuppressWarnings("ConstantConditions")
188    public boolean isBinary() {
189        return hasContentType() &&
190                (contentType.toLowerCase().contains("application/octet-stream") ||
191                        contentType.toLowerCase().contains("image/") ||
192                        contentType.toLowerCase().contains("video/") ||
193                        contentType.toLowerCase().contains("audio/"));
194    }
195
196    /**
197     * Returns the content length in characters.
198     *
199     * @return The length of the content string, or 0 if content is null
200     */
201    public int contentLength() {
202        return content != null ? content.length() : 0;
203    }
204
205    /**
206     * Extracts the charset from the content type if specified.
207     *
208     * @return The charset name wrapped in Optional, or empty if not specified
209     */
210    public Optional<String> getCharset() {
211        if (!hasContentType()) {
212            return Optional.empty();
213        }
214        return AttributeParser.extractAttributeValue(contentType, "charset");
215    }
216
217    /**
218     * Returns the content or a default value if content is null.
219     *
220     * @param defaultContent The default content to return if content is null
221     * @return The content or the default
222     */
223    public String contentOrDefault(String defaultContent) {
224        return content != null ? content : defaultContent;
225    }
226
227    /**
228     * Returns the content type or a default value if content type is null.
229     *
230     * @param defaultContentType The default content type to return if contentType is null
231     * @return The content type or the default
232     */
233    public String contentTypeOrDefault(String defaultContentType) {
234        return contentType != null ? contentType : defaultContentType;
235    }
236
237    /**
238     * Returns the encoding or a default value if encoding is null.
239     *
240     * @param defaultEncoding The default encoding to return if encoding is null
241     * @return The encoding or the default
242     */
243    public String encodingOrDefault(String defaultEncoding) {
244        return encoding != null ? encoding : defaultEncoding;
245    }
246
247    /**
248     * Returns a copy of this body with new content.
249     *
250     * @param newContent The new content
251     * @return A new HTTPBody with the specified content and same contentType/encoding
252     */
253    public HTTPBody withContent(String newContent) {
254        return new HTTPBody(newContent, contentType, encoding);
255    }
256
257    /**
258     * Returns a copy of this body with a new content type.
259     *
260     * @param newContentType The new content type
261     * @return A new HTTPBody with the same content/encoding and specified content type
262     */
263    public HTTPBody withContentType(String newContentType) {
264        return new HTTPBody(content, newContentType, encoding);
265    }
266
267    /**
268     * Returns a copy of this body with a new encoding.
269     *
270     * @param newEncoding The new encoding
271     * @return A new HTTPBody with the same content/contentType and specified encoding
272     */
273    public HTTPBody withEncoding(String newEncoding) {
274        return new HTTPBody(content, contentType, newEncoding);
275    }
276
277    /**
278     * Returns a truncated version of the content for safe logging.
279     *
280     * @param maxLength The maximum length for the truncated content
281     * @return The content truncated to the specified length with "..." if truncated
282     */
283    public String contentTruncated(int maxLength) {
284        if (content == null) {
285            return "null";
286        }
287        if (content.length() <= maxLength) {
288            return content;
289        }
290        return content.substring(0, maxLength) + "...";
291    }
292}