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.json;
037
038
039
040import java.text.ParseException;
041import java.util.ArrayList;
042import java.util.Collections;
043import java.util.Date;
044import java.util.LinkedHashMap;
045import java.util.LinkedHashSet;
046import java.util.List;
047import java.util.Map;
048import java.util.Set;
049
050import com.unboundid.ldap.sdk.unboundidds.logs.LogException;
051import com.unboundid.ldap.sdk.unboundidds.logs.v2.LogField;
052import com.unboundid.ldap.sdk.unboundidds.logs.v2.LogMessage;
053import com.unboundid.util.Debug;
054import com.unboundid.util.NotExtensible;
055import com.unboundid.util.NotNull;
056import com.unboundid.util.Nullable;
057import com.unboundid.util.StaticUtils;
058import com.unboundid.util.ThreadSafety;
059import com.unboundid.util.ThreadSafetyLevel;
060import com.unboundid.util.json.JSONArray;
061import com.unboundid.util.json.JSONBoolean;
062import com.unboundid.util.json.JSONNumber;
063import com.unboundid.util.json.JSONObject;
064import com.unboundid.util.json.JSONString;
065import com.unboundid.util.json.JSONValue;
066
067import static com.unboundid.ldap.sdk.unboundidds.logs.v2.json.JSONLogMessages.*;
068
069
070
071/**
072 * This class provides a data structure that holds information about a
073 * JSON-formatted log message.
074 * <BR>
075 * <BLOCKQUOTE>
076 *   <B>NOTE:</B>  This class, and other classes within the
077 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
078 *   supported for use against Ping Identity, UnboundID, and
079 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
080 *   for proprietary functionality or for external specifications that are not
081 *   considered stable or mature enough to be guaranteed to work in an
082 *   interoperable way with other types of LDAP servers.
083 * </BLOCKQUOTE>
084 */
085@NotExtensible()
086@ThreadSafety(level=ThreadSafetyLevel.INTERFACE_THREADSAFE)
087public abstract class JSONLogMessage
088       implements LogMessage
089{
090  /**
091   * The serial version UID for this serializable class.
092   */
093  private static final long serialVersionUID = 997950529069507711L;
094
095
096
097  // The JSON object that contains an encoded representation of this log
098  // message.
099  @NotNull private final JSONObject jsonObject;
100
101  // The timestamp value for this log message.
102  private final long timestampValue;
103
104  // A map of the fields in this log message.
105  @NotNull private final Map<String,List<String>> logFields;
106
107  // A string representation of this log message.
108  @NotNull private final String logMessageString;
109
110  // The log type for this log message.
111  @Nullable private final String logType;
112
113
114
115  /**
116   * Creates a new JSON log message from the provided JSON object.
117   *
118   * @param  jsonObject  The JSON object that contains an encoded representation
119   *                     of this log message.  It must not be {@code null}.
120   *
121   * @throws  LogException  If the provided JSON object cannot be parsed as a
122   *                        valid log message.
123   */
124  protected JSONLogMessage(@NotNull final JSONObject jsonObject)
125            throws LogException
126  {
127    this.jsonObject = jsonObject;
128    logMessageString = jsonObject.toSingleLineString();
129
130    final JSONValue timestampJSONValue = jsonObject.getField(
131         JSONFormattedAccessLogFields.TIMESTAMP.getFieldName());
132    if (timestampJSONValue == null)
133    {
134      throw new LogException(logMessageString,
135           ERR_JSON_LOG_MESSAGE_MISSING_TIMESTAMP.get(logMessageString,
136                JSONFormattedAccessLogFields.TIMESTAMP.getFieldName()));
137    }
138
139    if (! (timestampJSONValue instanceof JSONString))
140    {
141      throw new LogException(logMessageString,
142           ERR_JSON_LOG_MESSAGE_TIMESTAMP_NOT_STRING.get(logMessageString,
143           JSONFormattedAccessLogFields.TIMESTAMP.getFieldName()));
144    }
145
146    try
147    {
148      timestampValue = StaticUtils.decodeRFC3339Time(
149           ((JSONString) timestampJSONValue).stringValue()).getTime();
150    }
151    catch (final ParseException e)
152    {
153      Debug.debugException(e);
154      throw new LogException(logMessageString,
155           ERR_JSON_LOG_MESSAGE_MALFORMED_TIMESTAMP.get(logMessageString,
156                JSONFormattedAccessLogFields.TIMESTAMP.getFieldName()),
157           e);
158    }
159
160
161    final Map<String,List<String>> fieldMap = new LinkedHashMap<>();
162    for (final Map.Entry<String,JSONValue> e :
163         jsonObject.getFields().entrySet())
164    {
165      fieldMap.put(e.getKey(), valueToStrings(e.getValue()));
166    }
167
168    logFields = Collections.unmodifiableMap(fieldMap);
169
170    logType = getString(JSONFormattedAccessLogFields.LOG_TYPE);
171  }
172
173
174
175  /**
176   * Retrieves a list of the string representations of the values represented by
177   * the provided JSON value.
178   *
179   * @param  value  The JSON value for which to obtain the string
180   *                representations.  It must not be {@code null}.
181   *
182   * @return  A list of the string representations of the values represented by
183   *          the provided JSON value.
184   */
185  @NotNull()
186  static List<String> valueToStrings(@NotNull final JSONValue value)
187  {
188    if (value instanceof JSONArray)
189    {
190      final JSONArray a = (JSONArray) value;
191      final List<JSONValue> valueList = a.getValues();
192      final List<String> valueStrings = new ArrayList<>(valueList.size());
193      for (final JSONValue v : valueList)
194      {
195        if (v instanceof JSONString)
196        {
197          valueStrings.add(((JSONString) v).stringValue());
198        }
199        else
200        {
201          valueStrings.add(v.toSingleLineString());
202        }
203      }
204
205      return Collections.unmodifiableList(valueStrings);
206    }
207    else if (value instanceof JSONString)
208    {
209      return Collections.singletonList(((JSONString) value).stringValue());
210    }
211    else
212    {
213      return Collections.singletonList(value.toSingleLineString());
214    }
215  }
216
217
218
219  /**
220   * Retrieves the JSON object that contains an encoded representation of this
221   * log message.
222   *
223   * @return  The JSON object that contains an encoded representation of this
224   *          log message.
225   */
226  @NotNull()
227  public final JSONObject getJSONObject()
228  {
229    return jsonObject;
230  }
231
232
233
234  /**
235   * {@inheritDoc}
236   */
237  @Override()
238  @NotNull()
239  public final Date getTimestamp()
240  {
241    return new Date(timestampValue);
242  }
243
244
245
246  /**
247   * Retrieves the type of logger with which this message is associated.
248   *
249   * @return  The type of logger with which this message is associated, or
250   *          {@code null} if it is not included in the log message.
251   */
252  @Nullable()
253  public final String getLogType()
254  {
255    return logType;
256  }
257
258
259
260  /**
261   * {@inheritDoc}
262   */
263  @Override()
264  @NotNull()
265  public final Map<String,List<String>> getFields()
266  {
267    return logFields;
268  }
269
270
271  /**
272   * {@inheritDoc}
273   */
274  @Override()
275  @Nullable()
276  public final Boolean getBoolean(@NotNull final LogField logField)
277         throws LogException
278  {
279    final JSONValue fieldValue = getFirstValue(logField);
280    if (fieldValue == null)
281    {
282      return null;
283    }
284
285    if (fieldValue instanceof JSONBoolean)
286    {
287      return ((JSONBoolean) fieldValue).booleanValue();
288    }
289    else if (fieldValue instanceof JSONString)
290    {
291      final String stringValue = ((JSONString) fieldValue).stringValue();
292      if (stringValue.equalsIgnoreCase("true"))
293      {
294        return Boolean.TRUE;
295      }
296      else if (stringValue.equalsIgnoreCase("false"))
297      {
298        return Boolean.FALSE;
299      }
300      else
301      {
302        throw new LogException(logMessageString,
303             ERR_JSON_LOG_MESSAGE_VALUE_NOT_BOOLEAN.get(logField.getFieldName(),
304                  logMessageString));
305      }
306    }
307    else
308    {
309      throw new LogException(logMessageString,
310           ERR_JSON_LOG_MESSAGE_VALUE_NOT_BOOLEAN.get(logField.getFieldName(),
311                logMessageString));
312    }
313  }
314
315
316
317  /**
318   * Retrieves the Boolean value of the specified field.
319   *
320   * @param  logField  The field for which to retrieve the Boolean value.
321   *
322   * @return  The Boolean value of the specified field, or {@code null} if the
323   *          field does not exist in the log message or cannot be parsed as a
324   *          Boolean.
325   */
326  @Nullable()
327  final Boolean getBooleanNoThrow(@NotNull final LogField logField)
328  {
329    try
330    {
331      return getBoolean(logField);
332    }
333    catch (final LogException e)
334    {
335      Debug.debugException(e);
336      return null;
337    }
338  }
339
340
341
342  /**
343   * {@inheritDoc}
344   */
345  @Override()
346  @Nullable()
347  public final Date getGeneralizedTime(@NotNull final LogField logField)
348         throws LogException
349  {
350    final JSONValue fieldValue = getFirstValue(logField);
351    if (fieldValue == null)
352    {
353      return null;
354    }
355
356    if (fieldValue instanceof JSONString)
357    {
358      final String stringValue = ((JSONString) fieldValue).stringValue();
359      try
360      {
361        return StaticUtils.decodeGeneralizedTime(stringValue);
362      }
363      catch (final Exception e)
364      {
365        Debug.debugException(e);
366        throw new LogException(logMessageString,
367             ERR_JSON_LOG_MESSAGE_VALUE_NOT_GENERALIZED_TIME.get(
368                  logField.getFieldName(), logMessageString),
369             e);
370      }
371    }
372    else
373    {
374      throw new LogException(logMessageString,
375           ERR_JSON_LOG_MESSAGE_VALUE_NOT_GENERALIZED_TIME.get(
376                logField.getFieldName(), logMessageString));
377    }
378  }
379
380
381
382  /**
383   * Retrieves the generalized time value of the specified field.
384   *
385   * @param  logField  The field for which to retrieve the generalized time
386   *                   value.
387   *
388   * @return  The generalized time value of the specified field, or {@code null}
389   *          if the field does not exist in the log message or cannot be parsed
390   *          as a timestamp in the generalized time format.
391   */
392  @Nullable()
393  final Date getGeneralizedTimeNoThrow(@NotNull final LogField logField)
394  {
395    try
396    {
397      return getGeneralizedTime(logField);
398    }
399    catch (final LogException e)
400    {
401      Debug.debugException(e);
402      return null;
403    }
404  }
405
406
407
408  /**
409   * {@inheritDoc}
410   */
411  @Override()
412  @Nullable()
413  public final Double getDouble(@NotNull final LogField logField)
414         throws LogException
415  {
416    final JSONValue fieldValue = getFirstValue(logField);
417    if (fieldValue == null)
418    {
419      return null;
420    }
421
422    if (fieldValue instanceof JSONNumber)
423    {
424      return ((JSONNumber) fieldValue).getValue().doubleValue();
425    }
426    else if (fieldValue instanceof JSONString)
427    {
428      final String stringValue = ((JSONString) fieldValue).stringValue();
429      try
430      {
431        return Double.valueOf(stringValue);
432      }
433      catch (final Exception e)
434      {
435        Debug.debugException(e);
436        throw new LogException(logMessageString,
437             ERR_JSON_LOG_MESSAGE_VALUE_NOT_FLOATING_POINT.get(
438                  logField.getFieldName(), logMessageString),
439             e);
440      }
441    }
442    else
443    {
444      throw new LogException(logMessageString,
445           ERR_JSON_LOG_MESSAGE_VALUE_NOT_FLOATING_POINT.get(
446                logField.getFieldName(), logMessageString));
447    }
448  }
449
450
451
452  /**
453   * Retrieves the floating-point value of the specified field.
454   *
455   * @param  logField  The field for which to retrieve the floating-point value.
456   *
457   * @return  The floating-point value of the specified field, or {@code null}
458   *          if the field does not exist in the log message or cannot be parsed
459   *          as a Double.
460   */
461  @Nullable()
462  final Double getDoubleNoThrow(@NotNull final LogField logField)
463  {
464    try
465    {
466      return getDouble(logField);
467    }
468    catch (final LogException e)
469    {
470      Debug.debugException(e);
471      return null;
472    }
473  }
474
475
476
477  /**
478   * {@inheritDoc}
479   */
480  @Override()
481  @Nullable()
482  public final Integer getInteger(@NotNull final LogField logField)
483         throws LogException
484  {
485    final JSONValue fieldValue = getFirstValue(logField);
486    if (fieldValue == null)
487    {
488      return null;
489    }
490
491    if (fieldValue instanceof JSONNumber)
492    {
493      try
494      {
495        return ((JSONNumber) fieldValue).getValue().intValueExact();
496      }
497      catch (final Exception e)
498      {
499        Debug.debugException(e);
500        throw new LogException(logMessageString,
501             ERR_JSON_LOG_MESSAGE_VALUE_NOT_INTEGER.get(
502                  logField.getFieldName(), logMessageString),
503             e);
504      }
505    }
506    else if (fieldValue instanceof JSONString)
507    {
508      final String stringValue = ((JSONString) fieldValue).stringValue();
509      try
510      {
511        return Integer.parseInt(stringValue);
512      }
513      catch (final Exception e)
514      {
515        Debug.debugException(e);
516        throw new LogException(logMessageString,
517             ERR_JSON_LOG_MESSAGE_VALUE_NOT_INTEGER.get(
518                  logField.getFieldName(), logMessageString),
519             e);
520      }
521    }
522    else
523    {
524      throw new LogException(logMessageString,
525           ERR_JSON_LOG_MESSAGE_VALUE_NOT_INTEGER.get(
526                logField.getFieldName(), logMessageString));
527    }
528  }
529
530
531
532  /**
533   * Retrieves the integer value of the specified field.
534   *
535   * @param  logField  The field for which to retrieve the integer value.
536   *
537   * @return  The integer value of the specified field, or {@code null} if the
538   *          field does not exist in the log message or cannot be parsed as an
539   *          {@code Integer}.
540   */
541  @Nullable()
542  final Integer getIntegerNoThrow(@NotNull final LogField logField)
543  {
544    try
545    {
546      return getInteger(logField);
547    }
548    catch (final LogException e)
549    {
550      Debug.debugException(e);
551      return null;
552    }
553  }
554
555
556
557  /**
558   * {@inheritDoc}
559   */
560  @Override()
561  @Nullable()
562  public final Long getLong(@NotNull final LogField logField)
563         throws LogException
564  {
565    final JSONValue fieldValue = getFirstValue(logField);
566    if (fieldValue == null)
567    {
568      return null;
569    }
570
571    if (fieldValue instanceof JSONNumber)
572    {
573      try
574      {
575        return ((JSONNumber) fieldValue).getValue().longValueExact();
576      }
577      catch (final Exception e)
578      {
579        Debug.debugException(e);
580        throw new LogException(logMessageString,
581             ERR_JSON_LOG_MESSAGE_VALUE_NOT_INTEGER.get(
582                  logField.getFieldName(), logMessageString),
583             e);
584      }
585    }
586    else if (fieldValue instanceof JSONString)
587    {
588      final String stringValue = ((JSONString) fieldValue).stringValue();
589      try
590      {
591        return Long.parseLong(stringValue);
592      }
593      catch (final Exception e)
594      {
595        Debug.debugException(e);
596        throw new LogException(logMessageString,
597             ERR_JSON_LOG_MESSAGE_VALUE_NOT_INTEGER.get(
598                  logField.getFieldName(), logMessageString),
599             e);
600      }
601    }
602    else
603    {
604      throw new LogException(logMessageString,
605           ERR_JSON_LOG_MESSAGE_VALUE_NOT_INTEGER.get(
606                logField.getFieldName(), logMessageString));
607    }
608  }
609
610
611
612  /**
613   * Retrieves the integer value of the specified field.
614   *
615   * @param  logField  The field for which to retrieve the integer value.
616   *
617   * @return  The integer value of the specified field, or {@code null} if the
618   *          field does not exist in the log message or cannot be parsed as a
619   *          {@code Long}.
620   */
621  @Nullable()
622  final Long getLongNoThrow(@NotNull final LogField logField)
623  {
624    try
625    {
626      return getLong(logField);
627    }
628    catch (final LogException e)
629    {
630      Debug.debugException(e);
631      return null;
632    }
633  }
634
635
636
637  /**
638   * {@inheritDoc}
639   */
640  @Override()
641  @Nullable()
642  public final Date getRFC3339Timestamp(@NotNull final LogField logField)
643         throws LogException
644  {
645    final JSONValue fieldValue = getFirstValue(logField);
646    if (fieldValue == null)
647    {
648      return null;
649    }
650
651    if (fieldValue instanceof JSONString)
652    {
653      final String stringValue = ((JSONString) fieldValue).stringValue();
654      try
655      {
656        return StaticUtils.decodeRFC3339Time(stringValue);
657      }
658      catch (final Exception e)
659      {
660        Debug.debugException(e);
661        throw new LogException(logMessageString,
662             ERR_JSON_LOG_MESSAGE_VALUE_NOT_RFC_3339_TIME.get(
663                  logField.getFieldName(), logMessageString),
664             e);
665      }
666    }
667    else
668    {
669      throw new LogException(logMessageString,
670           ERR_JSON_LOG_MESSAGE_VALUE_NOT_RFC_3339_TIME.get(
671                logField.getFieldName(), logMessageString));
672    }
673  }
674
675
676
677  /**
678   * Retrieves the RFC 3339 timestamp value of the specified field.
679   *
680   * @param  logField  The field for which to retrieve the RFC 3339 timestamp
681   *                   value.
682   *
683   * @return  The RFC 3339 timestamp value of the specified field, or
684   *          {@code null} if the field does not exist in the log message or
685   *          cannot be parsed as a timestamp in the RFC 3339 format.
686   */
687  @Nullable()
688  final Date getRFC3339TimestampNoThrow(@NotNull final LogField logField)
689  {
690    try
691    {
692      return getRFC3339Timestamp(logField);
693    }
694    catch (final LogException e)
695    {
696      Debug.debugException(e);
697      return null;
698    }
699  }
700
701
702
703  /**
704   * {@inheritDoc}
705   */
706  @Override()
707  @Nullable()
708  public final String getString(@NotNull final LogField logField)
709  {
710    final JSONValue fieldValue = getFirstValue(logField);
711    if (fieldValue == null)
712    {
713      return null;
714    }
715
716    if (fieldValue instanceof JSONString)
717    {
718      return ((JSONString) fieldValue).stringValue();
719    }
720    else
721    {
722      return fieldValue.toSingleLineString();
723    }
724  }
725
726
727
728  /**
729   * Retrieves the list of values for the specified field as a list of strings.
730   * The values are expected to be in a JSON array whose values are all strings.
731   *
732   * @param  logField  The field for which to retrieve the list of values.
733   *
734   * @return  The list of values for the specified field as a list of strings,
735   *          or an empty list if the field is not present in the JSON object or
736   *          if it is not an array of strings.
737   */
738  @NotNull()
739  final List<String> getStringList(@NotNull final LogField logField)
740  {
741    final JSONValue fieldValue = jsonObject.getField(logField.getFieldName());
742    if (fieldValue == null)
743    {
744      return Collections.emptyList();
745    }
746
747    if (fieldValue instanceof JSONString)
748    {
749      return Collections.singletonList(((JSONString) fieldValue).stringValue());
750    }
751    else if (fieldValue instanceof JSONArray)
752    {
753      final List<JSONValue> values = ((JSONArray) fieldValue).getValues();
754      final List<String> stringValues = new ArrayList<>(values.size());
755      for (final JSONValue v : values)
756      {
757        if (v instanceof JSONString)
758        {
759          stringValues.add(((JSONString) v).stringValue());
760        }
761      }
762
763      return Collections.unmodifiableList(stringValues);
764    }
765
766    return Collections.emptyList();
767  }
768
769
770
771  /**
772   * Retrieves the set of values for the specified field as a set of strings.
773   * The values are expected to be in a JSON array whose values are all strings.
774   *
775   * @param  logField  The field for which to retrieve the set of values.
776   *
777   * @return  The set of values for the specified field as a set of strings, or
778   *          an empty set if the field is not present in the JSON object or if
779   *          it is not an array of strings.
780   */
781  @NotNull()
782  final Set<String> getStringSet(@NotNull final LogField logField)
783  {
784    return Collections.unmodifiableSet(
785         new LinkedHashSet<>(getStringList(logField)));
786  }
787
788
789
790  /**
791   * Retrieves the first value of the specified field from the log message
792   * object.
793   *
794   * @param  logField  The field for which to retrieve the first value.
795   *
796   * @return  The first value of the specified field, or {@code null} if the
797   *          field is not present in the log message object, or if its value is
798   *          an empty array.
799   */
800  @Nullable()
801  JSONValue getFirstValue(@NotNull final LogField logField)
802  {
803    final JSONValue value = jsonObject.getField(logField.getFieldName());
804    if (value == null)
805    {
806      return null;
807    }
808
809    if (value instanceof JSONArray)
810    {
811      final List<JSONValue> arrayValues = ((JSONArray) value).getValues();
812      if (arrayValues.isEmpty())
813      {
814        return null;
815      }
816      else
817      {
818        return arrayValues.get(0);
819      }
820    }
821    else
822    {
823      return value;
824    }
825  }
826
827
828
829  /**
830   * {@inheritDoc}
831   */
832  @Override()
833  @NotNull()
834  public final String toString()
835  {
836    return logMessageString;
837  }
838}