001/*
002 * Copyright 2022-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2022-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2022-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk.unboundidds.logs.v2.syntax;
037
038
039
040import java.text.DecimalFormat;
041
042import com.unboundid.util.ByteStringBuffer;
043import com.unboundid.util.Debug;
044import com.unboundid.util.NotNull;
045import com.unboundid.util.ThreadSafety;
046import com.unboundid.util.ThreadSafetyLevel;
047import com.unboundid.util.json.JSONBuffer;
048
049import static com.unboundid.ldap.sdk.unboundidds.logs.v2.syntax.
050                   LogSyntaxMessages.*;
051
052
053
054/**
055 * This class defines a log field syntax for values that are floating-point
056 * numbers.  This syntax does not support redacting or tokenizing individual
057 * components within the numbers.  Redacted floating-point values will have a
058 * string representation of "-999999.999999".  Tokenized floating-point string
059 * values will have a string representation of "-999999." followed by six digits
060 * that correspond to a token value generated from the actual value.
061 * <BR>
062 * <BLOCKQUOTE>
063 *   <B>NOTE:</B>  This class, and other classes within the
064 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
065 *   supported for use against Ping Identity, UnboundID, and
066 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
067 *   for proprietary functionality or for external specifications that are not
068 *   considered stable or mature enough to be guaranteed to work in an
069 *   interoperable way with other types of LDAP servers.
070 * </BLOCKQUOTE>
071 */
072@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
073public final class FloatingPointLogFieldSyntax
074       extends LogFieldSyntax<Double>
075{
076  /**
077   * The name for this syntax.
078   */
079  @NotNull public static final String SYNTAX_NAME = "floating-point";
080
081
082
083  /**
084   * The string representation that will be used for a floating-point value that
085   * is completely redacted.
086   */
087  @NotNull private static final String REDACTED_FLOATING_POINT_STRING =
088       "-999999.999999";
089
090
091
092  /**
093   * A singleton instance of this log field syntax.
094   */
095  @NotNull private static final FloatingPointLogFieldSyntax INSTANCE =
096       new FloatingPointLogFieldSyntax();
097
098
099
100  // Thread-local decimal formatters to use for formatting string
101  // representations.
102  @NotNull private final ThreadLocal<DecimalFormat> threadLocalFormatters;
103
104
105
106  /**
107   * Creates a new instance of this log field syntax implementation.
108   */
109  private FloatingPointLogFieldSyntax()
110  {
111    super(100);
112
113    threadLocalFormatters = new ThreadLocal<>();
114  }
115
116
117
118  /**
119   * Retrieves a singleton instance of this log field syntax.
120   *
121   * @return  A singleton instance of this log field syntax.
122   */
123  @NotNull()
124  public static FloatingPointLogFieldSyntax getInstance()
125  {
126    return INSTANCE;
127  }
128
129
130
131  /**
132   * {@inheritDoc}
133   */
134  @Override()
135  @NotNull()
136  public String getSyntaxName()
137  {
138    return SYNTAX_NAME;
139  }
140
141
142
143  /**
144   * Appends a sanitized string representation of the provided float to the
145   * given buffer.
146   *
147   * @param  value   The value to be appended.
148   * @param  buffer  The buffer to which the string representation should be
149   *                 appended.  It must not be {@code null}.
150   */
151  public void valueToSanitizedString(final float value,
152                            @NotNull final ByteStringBuffer buffer)
153  {
154    buffer.append(getDecimalFormatter().format(value));
155  }
156
157
158
159  /**
160   * Appends a sanitized string representation of the provided double to the
161   * given buffer.
162   *
163   * @param  value   The value to be appended.
164   * @param  buffer  The buffer to which the string representation should be
165   *                 appended.  It must not be {@code null}.
166   */
167  public void valueToSanitizedString(final double value,
168                                     @NotNull final ByteStringBuffer buffer)
169  {
170    buffer.append(getDecimalFormatter().format(value));
171  }
172
173
174
175  /**
176   * {@inheritDoc}
177   */
178  @Override()
179  public void valueToSanitizedString(@NotNull final Double value,
180                                     @NotNull final ByteStringBuffer buffer)
181  {
182    buffer.append(getDecimalFormatter().format(value));
183  }
184
185
186
187  /**
188   * Retrieves a decimal formatter to use for formatting the string
189   * representations of floating-point values.
190   *
191   * @return  A decimal formatter to use for formatting the string
192   *          representations of floating-point values.
193   */
194  @NotNull()
195  private DecimalFormat getDecimalFormatter()
196  {
197    DecimalFormat formatter = threadLocalFormatters.get();
198    if (formatter == null)
199    {
200      formatter = new DecimalFormat("0.000");
201      threadLocalFormatters.set(formatter);
202    }
203
204    return formatter;
205  }
206
207
208
209  /**
210   * {@inheritDoc}
211   */
212  @Override()
213  public void logSanitizedFieldToTextFormattedLog(
214                   @NotNull final String fieldName,
215                   @NotNull final Double fieldValue,
216                   @NotNull final ByteStringBuffer buffer)
217  {
218    buffer.append(' ');
219    buffer.append(fieldName);
220    buffer.append('=');
221    buffer.append(getDecimalFormatter().format(fieldValue));
222  }
223
224
225
226  /**
227   * {@inheritDoc}
228   */
229  @Override()
230  public void logSanitizedFieldToJSONFormattedLog(
231                   @NotNull final String fieldName,
232                   @NotNull final Double fieldValue,
233                   @NotNull final JSONBuffer buffer)
234  {
235    buffer.appendNumber(fieldName, getDecimalFormatter().format(fieldValue));
236  }
237
238
239
240  /**
241   * {@inheritDoc}
242   */
243  @Override()
244  public void logSanitizedValueToJSONFormattedLog(
245              @NotNull final Double value,
246              @NotNull final JSONBuffer buffer)
247  {
248    buffer.appendNumber(getDecimalFormatter().format(value));
249  }
250
251
252
253  /**
254   * {@inheritDoc}
255   */
256  @Override()
257  @NotNull()
258  public Double parseValue(@NotNull final String valueString)
259         throws RedactedValueException, TokenizedValueException,
260                LogSyntaxException
261  {
262    try
263    {
264      return Double.parseDouble(valueString);
265    }
266    catch (final Exception e)
267    {
268      Debug.debugException(e);
269      if (valueStringIncludesRedactedComponent(valueString))
270      {
271        throw new RedactedValueException(
272             ERR_FP_LOG_SYNTAX_CANNOT_PARSE_REDACTED.get(), e);
273      }
274      else if (valueStringIncludesTokenizedComponent(valueString))
275      {
276        throw new TokenizedValueException(
277             ERR_FP_LOG_SYNTAX_CANNOT_PARSE_TOKENIZED.get(), e);
278      }
279      else
280      {
281        throw new LogSyntaxException(
282             ERR_FP_LOG_SYNTAX_CANNOT_PARSE.get(), e);
283      }
284    }
285  }
286
287
288
289  /**
290   * {@inheritDoc}
291   */
292  @Override()
293  public boolean valueStringIsCompletelyRedacted(
294                      @NotNull final String valueString)
295  {
296    return valueString.equals(REDACTED_STRING) ||
297         valueString.equals(REDACTED_FLOATING_POINT_STRING);
298  }
299
300
301
302  /**
303   * {@inheritDoc}
304   */
305  @Override()
306  public boolean completelyRedactedValueConformsToSyntax()
307  {
308    return true;
309  }
310
311
312
313  /**
314   * {@inheritDoc}
315   */
316  @Override()
317  public void redactEntireValue(@NotNull final ByteStringBuffer buffer)
318  {
319    buffer.append(REDACTED_FLOATING_POINT_STRING);
320  }
321
322
323
324  /**
325   * {@inheritDoc}
326   */
327  @Override()
328  public void logCompletelyRedactedFieldToTextFormattedLog(
329                   @NotNull final String fieldName,
330                   @NotNull final ByteStringBuffer buffer)
331  {
332    buffer.append(' ');
333    buffer.append(fieldName);
334    buffer.append('=');
335    buffer.append(REDACTED_FLOATING_POINT_STRING);
336  }
337
338
339
340  /**
341   * {@inheritDoc}
342   */
343  @Override()
344  public void logCompletelyRedactedFieldToJSONFormattedLog(
345                   @NotNull final String fieldName,
346                   @NotNull final JSONBuffer buffer)
347  {
348    buffer.appendNumber(fieldName, REDACTED_FLOATING_POINT_STRING);
349  }
350
351
352
353  /**
354   * {@inheritDoc}
355   */
356  @Override()
357  public void logCompletelyRedactedValueToJSONFormattedLog(
358                   @NotNull final JSONBuffer buffer)
359  {
360    buffer.appendNumber(REDACTED_FLOATING_POINT_STRING);
361  }
362
363
364
365  /**
366   * {@inheritDoc}
367   */
368  @Override()
369  public boolean supportsRedactedComponents()
370  {
371    return false;
372  }
373
374
375
376  /**
377   * {@inheritDoc}
378   */
379  @Override()
380  public boolean valueStringIncludesRedactedComponent(
381                      @NotNull final String valueString)
382  {
383    return valueStringIsCompletelyRedacted(valueString);
384  }
385
386
387
388  /**
389   * {@inheritDoc}
390   */
391  @Override()
392  public boolean valueWithRedactedComponentsConformsToSyntax()
393  {
394    return true;
395  }
396
397
398
399  /**
400   * {@inheritDoc}
401   */
402  @Override()
403  public void logRedactedComponentsFieldToTextFormattedLog(
404                   @NotNull final String fieldName,
405                   @NotNull final Double fieldValue,
406                   @NotNull final ByteStringBuffer buffer)
407  {
408    logCompletelyRedactedFieldToTextFormattedLog(fieldName, buffer);
409  }
410
411
412
413  /**
414   * {@inheritDoc}
415   */
416  @Override()
417  public void logRedactedComponentsFieldToJSONFormattedLog(
418                   @NotNull final String fieldName,
419                   @NotNull final Double fieldValue,
420                   @NotNull final JSONBuffer buffer)
421  {
422    logCompletelyRedactedFieldToJSONFormattedLog(fieldName, buffer);
423  }
424
425
426
427  /**
428   * {@inheritDoc}
429   */
430  @Override()
431  public void logRedactedComponentsValueToJSONFormattedLog(
432                   @NotNull final Double value,
433                   @NotNull final JSONBuffer buffer)
434  {
435    logCompletelyRedactedValueToJSONFormattedLog(buffer);
436  }
437
438
439
440  /**
441   * {@inheritDoc}
442   */
443  @Override()
444  public boolean valueStringIsCompletelyTokenized(
445                      @NotNull final String valueString)
446  {
447    if (super.valueStringIsCompletelyTokenized(valueString))
448    {
449      return true;
450    }
451
452    return ((valueString.length() == 14) &&
453         valueString.startsWith("-999999.") &&
454         (! valueString.equals(REDACTED_FLOATING_POINT_STRING)));
455  }
456
457
458
459  /**
460   * {@inheritDoc}
461   */
462  @Override()
463  public boolean completelyTokenizedValueConformsToSyntax()
464  {
465    return true;
466  }
467
468
469
470  /**
471   * {@inheritDoc}
472   */
473  @Override()
474  public void tokenizeEntireValue(@NotNull final Double value,
475                                  @NotNull final byte[] pepper,
476                                  @NotNull final ByteStringBuffer buffer)
477  {
478    // Get the bytes that comprise the bitwise encoding of the provided value.
479    final long valueBitsLong = Double.doubleToLongBits(value);
480    final byte[] valueBytes =
481    {
482      (byte) ((valueBitsLong >> 56) & 0xFFL),
483      (byte) ((valueBitsLong >> 48) & 0xFFL),
484      (byte) ((valueBitsLong >> 40) & 0xFFL),
485      (byte) ((valueBitsLong >> 32) & 0xFFL),
486      (byte) ((valueBitsLong >> 24) & 0xFFL),
487      (byte) ((valueBitsLong >> 16) & 0xFFL),
488      (byte) ((valueBitsLong >> 8) & 0xFFL),
489      (byte) (valueBitsLong & 0xFFL)
490    };
491
492
493    // Concatenate the value bytes and the pepper and compute a SHA-256 digest
494    // of the result.
495    final byte[] tokenDigest;
496    final ByteStringBuffer tempBuffer = getTemporaryBuffer();
497    try
498    {
499      tempBuffer.append(valueBytes);
500      tempBuffer.append(pepper);
501      tokenDigest = sha256(tempBuffer);
502    }
503    finally
504    {
505      releaseTemporaryBuffer(tempBuffer);
506    }
507
508
509    // Use the first four bytes of the token digest to generate a positive
510    // integer whose string representation is exactly ten digits long.  To do
511    // this, AND the first byte with 0x7F (which will make it positive) and OR
512    // the first byte with 0x40 (which will ensure that the value will be
513    // greater than or equal to 1073741824, and we already know that int
514    // values cannot exceed 2147483647, so that means it will be exactly ten
515    // digits).
516    final int fractionalDigitsInt =
517         (((tokenDigest[0] & 0x7F) | 0x40) << 24) |
518         ((tokenDigest[1] & 0xFF) << 16) |
519         ((tokenDigest[2] & 0xFF) << 8) |
520         (tokenDigest[3] & 0xFF);
521
522
523    // Take the last six digits of the string representation of the generated
524    // integer.
525    String fractionalDigits = String.valueOf(fractionalDigitsInt).substring(4);
526
527
528    // Make sure that the resulting six-digit string is not "999999", so that
529    // the tokenized value won't be confused with a redacted value.
530    if (fractionalDigits.equals("999999"))
531    {
532      fractionalDigits = "000000";
533    }
534
535
536    // Finally, generate the tokenized representation.  It will be "-999999."
537    // followed by the fractional digits generated above.
538    buffer.append("-999999.");
539    buffer.append(fractionalDigits);
540  }
541
542
543
544  /**
545   * {@inheritDoc}
546   */
547  @Override()
548  public void logCompletelyTokenizedFieldToTextFormattedLog(
549                   @NotNull final String fieldName,
550                   @NotNull final Double fieldValue,
551                   @NotNull final byte[] pepper,
552                   @NotNull final ByteStringBuffer buffer)
553  {
554    buffer.append(' ');
555    buffer.append(fieldName);
556    buffer.append('=');
557    tokenizeEntireValue(fieldValue, pepper, buffer);
558  }
559
560
561
562  /**
563   * {@inheritDoc}
564   */
565  @Override()
566  public void logCompletelyTokenizedFieldToJSONFormattedLog(
567                   @NotNull final String fieldName,
568                   @NotNull final Double fieldValue,
569                   @NotNull final byte[] pepper,
570                   @NotNull final JSONBuffer buffer)
571  {
572    buffer.appendNumber(fieldName, tokenizeEntireValue(fieldValue, pepper));
573  }
574
575
576
577  /**
578   * {@inheritDoc}
579   */
580  @Override()
581  public void logCompletelyTokenizedValueToJSONFormattedLog(
582                   @NotNull final Double value,
583                   @NotNull final byte[] pepper,
584                   @NotNull final JSONBuffer buffer)
585  {
586    buffer.appendNumber(tokenizeEntireValue(value, pepper));
587  }
588
589
590
591  /**
592   * {@inheritDoc}
593   */
594  @Override()
595  public boolean supportsTokenizedComponents()
596  {
597    return false;
598  }
599
600
601
602  /**
603   * {@inheritDoc}
604   */
605  @Override()
606  public boolean valueStringIncludesTokenizedComponent(
607                      @NotNull final String valueString)
608  {
609    return valueStringIsCompletelyTokenized(valueString);
610  }
611
612
613
614  /**
615   * {@inheritDoc}
616   */
617  @Override()
618  public boolean valueWithTokenizedComponentsConformsToSyntax()
619  {
620    return true;
621  }
622
623
624
625  /**
626   * {@inheritDoc}
627   */
628  @Override()
629  public void logTokenizedComponentsFieldToTextFormattedLog(
630                   @NotNull final String fieldName,
631                   @NotNull final Double fieldValue,
632                   @NotNull final byte[] pepper,
633                   @NotNull final ByteStringBuffer buffer)
634  {
635    logCompletelyTokenizedFieldToTextFormattedLog(fieldName, fieldValue, pepper,
636         buffer);
637  }
638
639
640
641  /**
642   * {@inheritDoc}
643   */
644  @Override()
645  public void logTokenizedComponentsFieldToJSONFormattedLog(
646                   @NotNull final String fieldName,
647                   @NotNull final Double fieldValue,
648                   @NotNull final byte[] pepper,
649                   @NotNull final JSONBuffer buffer)
650  {
651    logCompletelyTokenizedFieldToJSONFormattedLog(fieldName, fieldValue, pepper,
652         buffer);
653  }
654
655
656
657  /**
658   * {@inheritDoc}
659   */
660  @Override()
661  public void logTokenizedComponentsValueToJSONFormattedLog(
662                   @NotNull final Double value,
663                   @NotNull final byte[] pepper,
664                   @NotNull final JSONBuffer buffer)
665  {
666    logCompletelyTokenizedValueToJSONFormattedLog(value, pepper, buffer);
667  }
668}