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}