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.marshall;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.*;
022import java.util.stream.*;
023
024/**
025 * Supported wire formats for {@link BitSet} values.
026 *
027 * <p>
028 * Used by {@link MarshallingContext.Builder#bitSetFormat(BitSetFormat)},
029 * {@link Marshalled#bitSetFormat()},
030 * {@link MarshalledProp#bitSetFormat()}, and
031 * {@link MarshalledConfig#bitSetFormat()} to control how {@link BitSet} values
032 * are written to text-based wire formats.
033 *
034 * <p>
035 * The default is {@link #INDICES} which emits the ascending set-bit indices as a comma-delimited token.
036 *
037 * <h5 class='topic'>Precedence (highest to lowest)</h5>
038 * <ol>
039 *    <li>{@link MarshalledProp#bitSetFormat() @MarshalledProp(bitSetFormat=…)} on the bean property.
040 *    <li>{@link Marshalled#bitSetFormat() @Marshalled(bitSetFormat=…)} on the bean class.
041 *    <li>{@link MarshalledConfig#bitSetFormat() @MarshalledConfig(bitSetFormat=…)} on
042 *       <code><ja>@Rest</ja></code>-annotated classes / methods.
043 *    <li>Programmatic {@link MarshallingContext.Builder#bitSetFormat(BitSetFormat)}.
044 *    <li>Environment variable <c>MarshallingContext.bitSetFormat</c>.
045 *    <li>The default constant ({@link #INDICES}).
046 * </ol>
047 *
048 * <h5 class='topic'>Parsing</h5>
049 *
050 * <p>
051 * Parsing is directed by the configured constant — a serializer and parser sharing the same
052 * {@code BitSetFormat} round-trip cleanly.  The three textual shapes are ambiguous with each other
053 * (a bare digit string is a valid index list, bit string, and hex token), so the configured format is
054 * honored rather than auto-detected.
055 */
056public enum BitSetFormat {
057
058   /** Sentinel meaning "no value configured" — falls through to the next-higher precedence level. */
059   NOT_SET,
060
061   /**
062    * Ascending set-bit indices as a comma-delimited token (the default).
063    *
064    * <h5 class='figure'>Example:</h5>
065    * <p class='bcode'>
066    *    0,2,5
067    * </p>
068    *
069    * <p>
070    * An empty {@link BitSet} emits the empty string.
071    */
072   INDICES,
073
074   /**
075    * Little-endian bit string — character at position <c>i</c> is <c>'1'</c> when bit <c>i</c> is set.
076    *
077    * <h5 class='figure'>Example:</h5>
078    * <p class='bcode'>
079    *    101001
080    * </p>
081    *
082    * <p>
083    * The string length equals {@link BitSet#length()}; an empty {@link BitSet} emits the empty string.
084    */
085   BITS,
086
087   /**
088    * Hex-encoded little-endian byte array ({@link BitSet#toByteArray()}).
089    *
090    * <h5 class='figure'>Example:</h5>
091    * <p class='bcode'>
092    *    25
093    * </p>
094    *
095    * <p>
096    * An empty {@link BitSet} emits the empty string.
097    */
098   HEX;
099
100   private static final char[] HEX_CHARS = "0123456789abcdef".toCharArray();
101
102   /**
103    * Formats the specified {@link BitSet} using this format.
104    *
105    * <p>
106    * {@link #NOT_SET} falls through to {@link #INDICES}.
107    *
108    * @param value The value to format.  Can be <jk>null</jk> (returns <jk>null</jk>).
109    * @param format The format to use.  Can be <jk>null</jk> (treated as {@link #INDICES}).
110    * @return The formatted wire representation, or <jk>null</jk> if {@code value} is <jk>null</jk>.
111    */
112   public static String format(BitSet value, BitSetFormat format) {
113      if (value == null)
114         return null;
115      var fmt = format == null ? INDICES : format;
116      return switch (fmt) {
117         case NOT_SET, INDICES -> value.stream().mapToObj(Integer::toString).collect(Collectors.joining(","));
118         case BITS -> formatBits(value);
119         case HEX -> formatHex(value.toByteArray());
120      };
121   }
122
123   private static String formatBits(BitSet value) {
124      var len = value.length();
125      var sb = new StringBuilder(len);
126      for (var i = 0; i < len; i++)
127         sb.append(value.get(i) ? '1' : '0');
128      return sb.toString();
129   }
130
131   private static String formatHex(byte[] bytes) {
132      var sb = new StringBuilder(bytes.length * 2);
133      for (var b : bytes) {
134         sb.append(HEX_CHARS[(b >> 4) & 0xF]);
135         sb.append(HEX_CHARS[b & 0xF]);
136      }
137      return sb.toString();
138   }
139
140   /**
141    * Parses the specified wire value into a {@link BitSet}.
142    *
143    * <p>
144    * Parsing is directed by the {@code format} hint (defaulting to {@link #INDICES}); the three textual
145    * shapes are mutually ambiguous, so the configured format is honored rather than auto-detected.
146    *
147    * @param value The wire value.  Can be <jk>null</jk> (returns <jk>null</jk>) or blank (returns an empty {@link BitSet}).
148    * @param format The configured format hint.  Can be <jk>null</jk> (treated as {@link #INDICES}).
149    * @return The parsed {@link BitSet}, or <jk>null</jk> if {@code value} is <jk>null</jk>.
150    * @throws IllegalArgumentException If the value does not match the configured textual shape.
151    */
152   public static BitSet parse(String value, BitSetFormat format) {
153      if (value == null)
154         return null;
155      var s = value.trim();
156      var fmt = format == null ? INDICES : format;
157      var bs = new BitSet();
158      if (s.isEmpty())
159         return bs;
160      try {
161         switch (fmt) {
162            case NOT_SET, INDICES -> {
163               for (var tok : s.split(","))
164                  bs.set(Integer.parseInt(tok.trim()));
165            }
166            case BITS -> {
167               for (var i = 0; i < s.length(); i++)
168                  if (s.charAt(i) == '1')
169                     bs.set(i);
170            }
171            case HEX -> {
172               return BitSet.valueOf(parseHex(s));
173            }
174         }
175      } catch (IllegalArgumentException e) {
176         throw iaex("Invalid BitSet value '%s' for format %s: %s", value, fmt, e.getMessage());
177      }
178      return bs;
179   }
180
181   private static byte[] parseHex(String s) {
182      if ((s.length() & 1) != 0)
183         throw iaex("Hex string must have an even number of characters");
184      var out = new byte[s.length() / 2];
185      for (var i = 0; i < out.length; i++) {
186         var hi = Character.digit(s.charAt(i * 2), 16);
187         var lo = Character.digit(s.charAt(i * 2 + 1), 16);
188         if (hi < 0 || lo < 0)
189            throw iaex("Invalid hex character");
190         out[i] = (byte) ((hi << 4) | lo);
191      }
192      return out;
193   }
194
195   /**
196    * Returns <jk>true</jk> if this format is numeric on the wire.
197    *
198    * <p>
199    * Always <jk>false</jk> — every constant emits a textual representation.
200    *
201    * @return <jk>false</jk>.
202    */
203   @SuppressWarnings({
204      "java:S3400", // Same rationale — must remain an instance method, not a constant, to match the cross-Format API contract.
205      "static-method" // Kept as an instance method for polymorphic-by-convention symmetry with the other Format classes (BigNumberFormat, FloatFormat, DurationFormat, etc.) where isNumeric() depends on the enum constant.
206   })
207   public boolean isNumeric() {
208      return false;
209   }
210}