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.matchingrules;
037
038
039
040import com.unboundid.asn1.ASN1OctetString;
041import com.unboundid.ldap.sdk.LDAPException;
042import com.unboundid.ldap.sdk.ResultCode;
043import com.unboundid.util.NotNull;
044import com.unboundid.util.ThreadSafety;
045import com.unboundid.util.ThreadSafetyLevel;
046
047import static com.unboundid.ldap.matchingrules.MatchingRuleMessages.*;
048
049
050
051/**
052 * This enum defines the policy that the {@link TelephoneNumberMatchingRule}
053 * should use when validating values in accordance with the syntax.  Regardless
054 * of the validation policy, the normalized representation of a value will be
055 * the provided value, converted to lowercase, with only spaces and hyphens
056 * removed.
057 */
058@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
059public enum TelephoneNumberValidationPolicy
060{
061  /**
062   * A policy that indicates that any non-empty printable string will be
063   * accepted.  Neither empty strings nor strings that contain characters from
064   * outside the set of printable characters will be accepted.
065   */
066  ALLOW_NON_EMPTY_PRINTABLE_STRING,
067
068
069
070  /**
071   * A policy that indicates that any non-empty printable string will be
072   * accepted, as long as it contains at least one digit.  Neither empty
073   * strings, strings nor strings that contain characters from outside the set
074   * of printable characters, nor strings without any digits will be accepted.
075   */
076  ALLOW_NON_EMPTY_PRINTABLE_STRING_WITH_AT_LEAST_ONE_DIGIT,
077
078
079
080  /**
081   * A policy that indicates that only values that strictly adhere to the
082   * X.520 specification will be accepted.  Only values that start with a
083   * plus sign, contain at least one digit, and contain only digits, spaces, and
084   * hyphens will be accepted.
085   */
086  ENFORCE_STRICT_X520_COMPLIANCE;
087
088
089
090  /**
091   * Validates the provided value to ensure that it satisfies this validation
092   * policy.
093   *
094   * @param  value        The value to be validated.  It must not be
095   *                      {@code null}.
096   * @param  isSubstring  Indicates whether the provided value represents a
097   *                      substring rather than a complete value.
098   *
099   * @throws  LDAPException  If the provided value is not acceptable as per the
100   *                         constraints of this policy.
101   */
102  public void validateValue(@NotNull final ASN1OctetString value,
103                            final boolean isSubstring)
104         throws LDAPException
105  {
106    switch (this)
107    {
108      case ALLOW_NON_EMPTY_PRINTABLE_STRING:
109        validateNonEmptyPrintableString(value, isSubstring, false);
110        break;
111
112      case ALLOW_NON_EMPTY_PRINTABLE_STRING_WITH_AT_LEAST_ONE_DIGIT:
113        validateNonEmptyPrintableString(value, isSubstring, true);
114        break;
115
116      case ENFORCE_STRICT_X520_COMPLIANCE:
117      default:
118        validateX520Compliant(value, isSubstring);
119        break;
120    }
121  }
122
123
124
125  /**
126   * Validates that the provided value is valid in accordance with a policy that
127   * requires a non-empty printable string.
128   *
129   * @param  value                   The value to be validated.  It must not be
130   *                                 {@code null}.
131   * @param  isSubstring             Indicates whether the value represents a
132   *                                 substring rather than a complete value.
133   * @param  requireAtLeastOneDigit  Indicates whether to require the value to
134   *                                 contain at least one digit.  This only
135   *                                 applies if {@code isSubstring} is false.
136   *
137   * @throws  LDAPException  If the provided value is not valid.
138   */
139  private static void validateNonEmptyPrintableString(
140               @NotNull final ASN1OctetString value,
141               final boolean isSubstring,
142               final boolean requireAtLeastOneDigit)
143          throws LDAPException
144  {
145    // Make sure that the value is not empty.
146    final byte[] valueBytes = value.getValue();
147    if (valueBytes.length == 0)
148    {
149      if (isSubstring)
150      {
151        // Substring components must not be empty.
152        throw new LDAPException(ResultCode.INAPPROPRIATE_MATCHING,
153             ERR_TELEPHONE_NUMBER_VALIDATION_EMPTY_SUBSTRING.get());
154      }
155      else
156      {
157        // Telephone number values must not be empty.
158        throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
159             ERR_TELEPHONE_NUMBER_VALIDATION_EMPTY_VALUE.get());
160      }
161    }
162
163
164    // Iterate through the bytes of the value and make sure they are all
165    // printable.  Also, check to see if we find any digits.
166    boolean digitFound = false;
167    for (int i=0; i < valueBytes.length; i++)
168    {
169      final byte b = valueBytes[i];
170      if ((b >= '0') && (b <= '9'))
171      {
172        // It's a numeric digit, which is always allowed.
173        digitFound = true;
174      }
175      else if (((b >= 'a') && (b <= 'z')) || ((b >= 'A') && (b <= 'Z')))
176      {
177        // It's an alphabetic character, which is allowed as per the policy.
178      }
179      else
180      {
181        switch (b)
182        {
183          case '\'':
184          case '(':
185          case ')':
186          case '+':
187          case ',':
188          case '-':
189          case '.':
190          case '=':
191          case '/':
192          case ':':
193          case '?':
194          case ' ':
195            // These characters are all allowed.
196            break;
197          default:
198            // This character is not allowed.
199            if (isSubstring)
200            {
201              throw new LDAPException(ResultCode.INAPPROPRIATE_MATCHING,
202                   ERR_TELEPHONE_NUMBER_VALIDATION_NON_PRINTABLE_SUB_CHAR.get(
203                        value.stringValue(), i));
204            }
205            else
206            {
207              throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
208                   ERR_TELEPHONE_NUMBER_VALIDATION_NON_PRINTABLE_CHAR.get(
209                        value.stringValue(), i));
210            }
211        }
212      }
213    }
214
215
216    // If we should require a digit, then make sure we found one.
217    if (requireAtLeastOneDigit && (! isSubstring) && (! digitFound))
218    {
219      throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
220           ERR_TELEPHONE_NUMBER_VALIDATION_NO_DIGITS.get(value.stringValue()));
221    }
222  }
223
224
225
226  /**
227   * Validates that the provided value is a valid telephone number using the
228   * strict specification defined in X.520.
229   *
230   * @param  value        The value to be validated.  It must not be
231   *                      {@code null}.
232   * @param  isSubstring  Indicates whether the value represents a substring
233   *                      rather than a complete value.
234   *
235   * @throws  LDAPException  If the provided value is not valid.
236   */
237  private static void validateX520Compliant(
238               @NotNull final ASN1OctetString value,
239               final boolean isSubstring)
240          throws LDAPException
241  {
242    // Make sure that the value is not empty.
243    final byte[] valueBytes = value.getValue();
244    if (valueBytes.length == 0)
245    {
246      if (isSubstring)
247      {
248        // Substring components must not be empty.
249        throw new LDAPException(ResultCode.INAPPROPRIATE_MATCHING,
250             ERR_TELEPHONE_NUMBER_VALIDATION_EMPTY_SUBSTRING.get());
251      }
252      else
253      {
254        // Telephone number values must not be empty.
255        throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
256             ERR_TELEPHONE_NUMBER_VALIDATION_EMPTY_VALUE.get());
257      }
258    }
259
260
261    // If the value is not a substring, then make sure it starts with a plus
262    // sign.
263    if ((! isSubstring) && (valueBytes[0] != '+'))
264    {
265      throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
266           ERR_TELEPHONE_NUMBER_VALIDATION_MISSING_PLUS.get(
267                value.stringValue()));
268    }
269
270
271    // Iterate through the bytes of the value and make sure it only contains a
272    // plus sign, one or more numeric digits, and optionally spaces and/or
273    // dashes.  A plus sign will only be allowed at position zero, and digits,
274    // spaces, and hyphens will be allowed anywhere.
275    boolean digitFound = false;
276    for (int i=0; i < valueBytes.length; i++)
277    {
278      final byte b = valueBytes[i];
279      if (b == '+')
280      {
281        if (i != 0)
282        {
283          if (isSubstring)
284          {
285            throw new LDAPException(ResultCode.INAPPROPRIATE_MATCHING,
286                 ERR_TELEPHONE_NUMBER_VALIDATION_NON_FIRST_PLUS_SUB.get(
287                      value.stringValue(), i));
288          }
289          else
290          {
291            throw new LDAPException(ResultCode.INAPPROPRIATE_MATCHING,
292                 ERR_TELEPHONE_NUMBER_VALIDATION_NON_FIRST_PLUS.get(
293                      value.stringValue(), i));
294          }
295        }
296      }
297      else if ((b >= '0') && (b <= '9'))
298      {
299        digitFound = true;
300      }
301      else if ((b == ' ') || (b == '-'))
302      {
303        // These are always allowed and always ignored.
304      }
305      else
306      {
307        if (isSubstring)
308        {
309            throw new LDAPException(ResultCode.INAPPROPRIATE_MATCHING,
310                 ERR_TELEPHONE_NUMBER_VALIDATION_INVALID_CHAR_SUB.get(
311                      value.stringValue(), i));
312        }
313        else
314        {
315            throw new LDAPException(ResultCode.INAPPROPRIATE_MATCHING,
316                 ERR_TELEPHONE_NUMBER_VALIDATION_INVALID_CHAR.get(
317                      value.stringValue(), i));
318        }
319      }
320    }
321
322
323    // If we didn't find any digits, then that's an error, even for a substring
324    // assertion.
325    if (! digitFound)
326    {
327      if (isSubstring)
328      {
329        throw new LDAPException(ResultCode.INAPPROPRIATE_MATCHING,
330             ERR_TELEPHONE_NUMBER_VALIDATION_NO_DIGITS_SUB.get(
331                  value.stringValue()));
332      }
333      else
334      {
335        throw new LDAPException(ResultCode.INAPPROPRIATE_MATCHING,
336             ERR_TELEPHONE_NUMBER_VALIDATION_NO_DIGITS.get(
337                  value.stringValue()));
338      }
339    }
340  }
341}