001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest.client;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.*;
022
023/**
024 * Per-tier formatter for JUL-level-driven REST client debug logging.
025 *
026 * <p>
027 * The client debug pipeline invokes these methods cumulatively based on the resolved logger level:
028 * <ul>
029 *    <li>{@link #formatBasic(RestRequest, RestResponse)} at {@code INFO}.
030 *    <li>{@link #formatHeaders(RestRequest, RestResponse)} added at {@code FINE}.
031 *    <li>{@link #formatBody(RestRequest, RestResponse)} added at {@code FINEST}.
032 * </ul>
033 *
034 * @since 10.0.0
035 */
036public interface RestClientDebugFormatter {
037
038   /**
039    * Renders the basic ({@code INFO}-tier) portion of a debug record.
040    *
041    * @param req The request.
042    * @param res The response.
043    * @return The formatted message chunk.
044    */
045   String formatBasic(RestRequest req, RestResponse res);
046
047   /**
048    * Renders the headers ({@code FINE}-tier) portion of a debug record.
049    *
050    * @param req The request.
051    * @param res The response.
052    * @return The formatted message chunk.
053    */
054   default String formatHeaders(RestRequest req, RestResponse res) {
055      return "";
056   }
057
058   /**
059    * Renders the body ({@code FINEST}-tier) portion of a debug record.
060    *
061    * @param req The request.
062    * @param res The response.
063    * @return The formatted message chunk.
064    */
065   default String formatBody(RestRequest req, RestResponse res) {
066      return "";
067   }
068
069   /**
070    * Maximum number of request/response body bytes to capture for {@code FINEST} rendering.
071    *
072    * @return The capture cap in bytes.
073    */
074   default int bodyCap() {
075      return 8 * 1024;
076   }
077
078   /**
079    * Returns <jk>true</jk> if a body with the given content type is worth rendering as text (as opposed to being
080    * binary/unknown).
081    *
082    * <p>
083    * This is a text-vs-binary renderability predicate, <b>not</b> a redaction allowlist. It parses only the media type
084    * (type/subtype, ignoring parameters such as {@code ; charset=utf-8}), case-folds, and treats the following as
085    * renderable: {@code text/*}, {@code application/json}, {@code application/xml}, any {@code +json}/{@code +xml}
086    * suffix, and {@code application/x-www-form-urlencoded}. {@code multipart/form-data} is explicitly non-renderable.
087    * An absent or blank content type is conservatively treated as non-renderable.
088    *
089    * <p>
090    * {@code Content-Encoding} is not visible to this one-argument predicate; a caller that dumps bodies must separately
091    * treat any non-identity encoding (e.g. {@code gzip}) as non-renderable.
092    *
093    * <p>
094    * Keep byte-identical to {@code org.apache.juneau.rest.server.logging.RestDebugFormatter}'s counterpart.
095    *
096    * @param contentType The body's content type. Can be <jk>null</jk> (returns <jk>false</jk>).
097    * @return <jk>true</jk> if the content type is renderable as text.
098    */
099   default boolean isBodyRenderable(String contentType) {
100      if (contentType == null)
101         return false;
102      var ct = contentType.trim();
103      var semi = ct.indexOf(';');
104      if (semi >= 0)
105         ct = ct.substring(0, semi).trim();
106      ct = ct.toLowerCase(Locale.ROOT);
107      if (ct.isEmpty())
108         return false;
109      if (ct.startsWith("text/"))
110         return true;
111      if (eq(ct, "multipart/form-data"))
112         return false;
113      if (eqa(ct, "application/json", "application/xml", "application/x-www-form-urlencoded"))
114         return true;
115      return ct.endsWith("+json") || ct.endsWith("+xml");
116   }
117}