001/*
002 * Copyright 2008-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2008-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) 2008-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.Debug;
044import com.unboundid.util.NotNull;
045import com.unboundid.util.Nullable;
046import com.unboundid.util.StaticUtils;
047import com.unboundid.util.ThreadSafety;
048import com.unboundid.util.ThreadSafetyLevel;
049
050import static com.unboundid.ldap.matchingrules.MatchingRuleMessages.*;
051
052
053
054/**
055 * This class provides an implementation of a matching rule that performs
056 * equality and ordering comparisons against values that should be integers.
057 * Substring matching is not supported.
058 */
059@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
060public final class IntegerMatchingRule
061       extends MatchingRule
062{
063  /**
064   * The singleton instance that will be returned from the {@code getInstance}
065   * method.
066   */
067  @NotNull private static final IntegerMatchingRule INSTANCE =
068       new IntegerMatchingRule();
069
070
071
072  /**
073   * The name for the integerMatch equality matching rule.
074   */
075  @NotNull public static final String EQUALITY_RULE_NAME = "integerMatch";
076
077
078
079  /**
080   * The name for the integerMatch equality matching rule, formatted in all
081   * lowercase characters.
082   */
083  @NotNull static final String LOWER_EQUALITY_RULE_NAME =
084       StaticUtils.toLowerCase(EQUALITY_RULE_NAME);
085
086
087
088  /**
089   * The OID for the integerMatch equality matching rule.
090   */
091  @NotNull public static final String EQUALITY_RULE_OID = "2.5.13.14";
092
093
094
095  /**
096   * The name for the integerOrderingMatch ordering matching rule.
097   */
098  @NotNull public static final String ORDERING_RULE_NAME =
099       "integerOrderingMatch";
100
101
102
103  /**
104   * The name for the integerOrderingMatch ordering matching rule, formatted
105   * in all lowercase characters.
106   */
107  @NotNull static final String LOWER_ORDERING_RULE_NAME =
108       StaticUtils.toLowerCase(ORDERING_RULE_NAME);
109
110
111
112  /**
113   * The OID for the integerOrderingMatch ordering matching rule.
114   */
115  @NotNull public static final String ORDERING_RULE_OID = "2.5.13.15";
116
117
118
119  /**
120   * The serial version UID for this serializable class.
121   */
122  private static final long serialVersionUID = -9056942146971528818L;
123
124
125
126  /**
127   * Creates a new instance of this integer matching rule.
128   */
129  public IntegerMatchingRule()
130  {
131    // No implementation is required.
132  }
133
134
135
136  /**
137   * Retrieves a singleton instance of this matching rule.
138   *
139   * @return  A singleton instance of this matching rule.
140   */
141  @NotNull()
142  public static IntegerMatchingRule getInstance()
143  {
144    return INSTANCE;
145  }
146
147
148
149  /**
150   * {@inheritDoc}
151   */
152  @Override()
153  @NotNull()
154  public String getEqualityMatchingRuleName()
155  {
156    return EQUALITY_RULE_NAME;
157  }
158
159
160
161  /**
162   * {@inheritDoc}
163   */
164  @Override()
165  @NotNull()
166  public String getEqualityMatchingRuleOID()
167  {
168    return EQUALITY_RULE_OID;
169  }
170
171
172
173  /**
174   * {@inheritDoc}
175   */
176  @Override()
177  @NotNull()
178  public String getOrderingMatchingRuleName()
179  {
180    return ORDERING_RULE_NAME;
181  }
182
183
184
185  /**
186   * {@inheritDoc}
187   */
188  @Override()
189  @NotNull()
190  public String getOrderingMatchingRuleOID()
191  {
192    return ORDERING_RULE_OID;
193  }
194
195
196
197  /**
198   * {@inheritDoc}
199   */
200  @Override()
201  @Nullable()
202  public String getSubstringMatchingRuleName()
203  {
204    return null;
205  }
206
207
208
209  /**
210   * {@inheritDoc}
211   */
212  @Override()
213  @Nullable()
214  public String getSubstringMatchingRuleOID()
215  {
216    return null;
217  }
218
219
220
221  /**
222   * {@inheritDoc}
223   */
224  @Override()
225  public boolean valuesMatch(@NotNull final ASN1OctetString value1,
226                             @NotNull final ASN1OctetString value2)
227         throws LDAPException
228  {
229    return normalize(value1).equals(normalize(value2));
230  }
231
232
233
234  /**
235   * {@inheritDoc}
236   */
237  @Override()
238  public boolean matchesAnyValue(@NotNull final ASN1OctetString assertionValue,
239                      @NotNull final ASN1OctetString[] attributeValues)
240         throws LDAPException
241  {
242    if ((assertionValue == null) || (attributeValues == null) ||
243        (attributeValues.length == 0))
244    {
245      return false;
246    }
247
248    final ASN1OctetString normalizedAssertionValue = normalize(assertionValue);
249
250    for (final ASN1OctetString attributeValue : attributeValues)
251    {
252      try
253      {
254        if (normalizedAssertionValue.equalsIgnoreType(
255             normalize(attributeValue)))
256        {
257          return true;
258        }
259      }
260      catch (final Exception e)
261      {
262        Debug.debugException(e);
263      }
264    }
265
266    return false;
267  }
268
269
270
271  /**
272   * {@inheritDoc}
273   */
274  @Override()
275  public boolean matchesSubstring(@NotNull final ASN1OctetString value,
276                                  @Nullable final ASN1OctetString subInitial,
277                                  @Nullable final ASN1OctetString[] subAny,
278                                  @Nullable final ASN1OctetString subFinal)
279         throws LDAPException
280  {
281    throw new LDAPException(ResultCode.INAPPROPRIATE_MATCHING,
282                            ERR_INTEGER_SUBSTRING_MATCHING_NOT_SUPPORTED.get());
283  }
284
285
286
287  /**
288   * {@inheritDoc}
289   */
290  @Override()
291  public int compareValues(@NotNull final ASN1OctetString value1,
292                           @NotNull final ASN1OctetString value2)
293         throws LDAPException
294  {
295    final byte[] norm1Bytes = normalize(value1).getValue();
296    final byte[] norm2Bytes = normalize(value2).getValue();
297
298    if (norm1Bytes[0] == '-')
299    {
300      if (norm2Bytes[0] == '-')
301      {
302        // Both values are negative.  The smaller negative is the larger value.
303        if (norm1Bytes.length < norm2Bytes.length)
304        {
305          return 1;
306        }
307        else if (norm1Bytes.length > norm2Bytes.length)
308        {
309          return -1;
310        }
311        else
312        {
313          for (int i=1; i < norm1Bytes.length; i++)
314          {
315            final int difference = norm2Bytes[i] - norm1Bytes[i];
316            if (difference != 0)
317            {
318              return difference;
319            }
320          }
321
322          return 0;
323        }
324      }
325      else
326      {
327        // The first is negative and the second is positive.
328        return -1;
329      }
330    }
331    else
332    {
333      if (norm2Bytes[0] == '-')
334      {
335        // The first is positive and the second is negative.
336        return 1;
337      }
338      else
339      {
340        // Both values are positive.
341        if (norm1Bytes.length < norm2Bytes.length)
342        {
343          return -1;
344        }
345        else if (norm1Bytes.length > norm2Bytes.length)
346        {
347          return 1;
348        }
349        else
350        {
351          for (int i=0; i < norm1Bytes.length; i++)
352          {
353            final int difference = norm1Bytes[i] - norm2Bytes[i];
354            if (difference != 0)
355            {
356              return difference;
357            }
358          }
359
360          return 0;
361        }
362      }
363    }
364  }
365
366
367
368  /**
369   * {@inheritDoc}
370   */
371  @Override()
372  @NotNull()
373  public ASN1OctetString normalize(@NotNull final ASN1OctetString value)
374         throws LDAPException
375  {
376    // It is likely that the provided value is already acceptable, so we should
377    // try to validate it without any unnecessary allocation.
378    final byte[] valueBytes = value.getValue();
379    if (valueBytes.length == 0)
380    {
381      throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
382                              ERR_INTEGER_ZERO_LENGTH_NOT_ALLOWED.get());
383    }
384
385    if ((valueBytes[0] == ' ') || (valueBytes[valueBytes.length-1] == ' '))
386    {
387      // There is either a leading or trailing space, which needs to be
388      // stripped out so we'll have to allocate memory for this.
389      final String valueStr = value.stringValue().trim();
390      if (valueStr.isEmpty())
391      {
392        throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
393                                ERR_INTEGER_ZERO_LENGTH_NOT_ALLOWED.get());
394      }
395
396      for (int i=0; i < valueStr.length(); i++)
397      {
398        switch (valueStr.charAt(i))
399        {
400          case '-':
401            // This is only acceptable as the first character, and only if it is
402            // followed by one or more other characters.
403            if ((i != 0) || (valueStr.length() == 1))
404            {
405              throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
406                   ERR_INTEGER_INVALID_CHARACTER.get(i));
407            }
408            break;
409
410          case '0':
411            // This is acceptable anywhere except the as first character unless
412            // it is the only character, or as the second character if the first
413            // character is a dash.
414            if (((i == 0) && (valueStr.length() > 1)) ||
415                ((i == 1) && (valueStr.charAt(0) == '-')))
416            {
417              throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
418                                      ERR_INTEGER_INVALID_LEADING_ZERO.get());
419            }
420            break;
421
422          case '1':
423          case '2':
424          case '3':
425          case '4':
426          case '5':
427          case '6':
428          case '7':
429          case '8':
430          case '9':
431            // These are always acceptable.
432            break;
433
434          default:
435            throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
436                                    ERR_INTEGER_INVALID_CHARACTER.get(i));
437        }
438      }
439
440      return new ASN1OctetString(valueStr);
441    }
442
443
444    // Perform the validation against the contents of the byte array.
445    for (int i=0; i < valueBytes.length; i++)
446    {
447      switch (valueBytes[i])
448      {
449        case '-':
450          // This is only acceptable as the first character, and only if it is
451          // followed by one or more other characters.
452          if ((i != 0) || (valueBytes.length == 1))
453          {
454            throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
455                 ERR_INTEGER_INVALID_CHARACTER.get(i));
456          }
457          break;
458
459        case '0':
460          // This is acceptable anywhere except the as first character unless
461          // it is the only character, or as the second character if the first
462          // character is a dash.
463          if (((i == 0) && (valueBytes.length > 1)) ||
464              ((i == 1) && (valueBytes[0] == '-')))
465          {
466            throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
467                                    ERR_INTEGER_INVALID_LEADING_ZERO.get());
468          }
469          break;
470
471        case '1':
472        case '2':
473        case '3':
474        case '4':
475        case '5':
476        case '6':
477        case '7':
478        case '8':
479        case '9':
480          // These are always acceptable.
481          break;
482
483        default:
484          throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
485                                  ERR_INTEGER_INVALID_CHARACTER.get(i));
486      }
487    }
488
489    return value;
490  }
491
492
493
494  /**
495   * {@inheritDoc}
496   */
497  @Override()
498  @NotNull()
499  public ASN1OctetString normalizeSubstring(
500                              @NotNull final ASN1OctetString value,
501                              final byte substringType)
502         throws LDAPException
503  {
504    throw new LDAPException(ResultCode.INAPPROPRIATE_MATCHING,
505                            ERR_INTEGER_SUBSTRING_MATCHING_NOT_SUPPORTED.get());
506  }
507}