001/*
002 * Copyright 2011-2016 UnboundID Corp.
003 *
004 * This program is free software; you can redistribute it and/or modify
005 * it under the terms of the GNU General Public License (GPLv2 only)
006 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
007 * as published by the Free Software Foundation.
008 *
009 * This program is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012 * GNU General Public License for more details.
013 *
014 * You should have received a copy of the GNU General Public License
015 * along with this program; if not, see <http://www.gnu.org/licenses>.
016 */
017
018package com.unboundid.scim.sdk;
019
020
021import com.unboundid.scim.schema.AttributeDescriptor;
022
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.Date;
027import java.util.HashSet;
028import java.util.List;
029import java.util.Set;
030
031import javax.xml.bind.DatatypeConverter;
032
033
034/**
035 * This class represents a System for Cross-Domain Identity Management (SCIM)
036 * attribute. Attributes are categorized as either single-valued or
037 * multi-valued. This class allows for the following kinds of attributes.
038 *
039 * <ol>
040 * <li>Simple type (String, Boolean, DateTime, Integer or Binary).
041 *     An example is the 'userName' attribute in the core schema.</li>
042 *
043 * <li>Complex type. An example is the 'name' attribute in the core
044 *     schema.</li>
045 *
046 * <li>Multi-valued simple type. Represented using multi-valued complex values,
047 *     because the values may have 'type' and 'primary' sub-attributes to
048 *     distinguish each primitive value. Examples of this are the 'emails' and
049 *     'photos' attributes in the core schema.</li>
050 *
051 * <li>Multi-valued complex type. An examples is the 'addresses' attribute in
052 *     the core schema.</li>
053 * </ol>
054 *
055 */
056public final class SCIMAttribute
057{
058  /**
059   * The mapping descriptor of this attribute.
060   */
061  private final AttributeDescriptor attributeDescriptor;
062
063  /**
064   * The value(s) of this attribute.
065   */
066  private final SCIMAttributeValue[] values;
067
068
069  /**
070   * Create a new instance of an attribute.
071   *
072   * @param descriptor    The mapping descriptor of this value.
073   * @param values        The value(s) of this attribute.
074   */
075  private SCIMAttribute(final AttributeDescriptor descriptor,
076                        final SCIMAttributeValue ... values)
077  {
078    this.attributeDescriptor = descriptor;
079    this.values = values;
080  }
081
082
083
084  /**
085   * Create an attribute.
086   *
087   * @param descriptor   The mapping descriptor for this attribute.
088   * @param values       The value(s) of this attribute.
089   *
090   * @return  A new attribute.
091   */
092  public static SCIMAttribute create(
093      final AttributeDescriptor descriptor, final SCIMAttributeValue... values)
094  {
095    // Make sure all values of a multi-valued attributes are represented using
096    // the special MultiValuedSCIMAttributeValue implementation.
097    SCIMAttributeValue[] valuesCopy;
098    if(values != null)
099    {
100      valuesCopy = new SCIMAttributeValue[values.length];
101      if(descriptor.isMultiValued())
102      {
103        int i = 0;
104        for(SCIMAttributeValue value : values)
105        {
106          if(!(value instanceof
107              MultiValuedSCIMAttributeValue))
108          {
109            if(value.isComplex())
110            {
111              valuesCopy[i++] =
112                  new MultiValuedSCIMAttributeValue(
113                      value.getAttributes());
114            }
115            else
116            {
117              // Try to convert to complex value by using the value
118              // sub-attribute.
119              SCIMAttribute valueSubAttribute;
120              try
121              {
122                valueSubAttribute =
123                    new SCIMAttribute(descriptor.getSubAttribute("value"),
124                        value);
125              }
126              catch (InvalidResourceException e)
127              {
128                // The multi-valued attribute did not define a value
129                // sub-attribute. Throw error.
130                throw new IllegalArgumentException("Values of multi-valued " +
131                    "attributes must be complex when the normative value " +
132                    "sub-attribute is not defined");
133              }
134
135              valuesCopy[i++] =
136                  new MultiValuedSCIMAttributeValue(
137                      Collections.singletonMap("value", valueSubAttribute));
138            }
139          }
140          else
141          {
142            valuesCopy[i++] = value;
143          }
144        }
145      }
146      else
147      {
148        System.arraycopy(values, 0, valuesCopy, 0, values.length);
149      }
150    }
151    else
152    {
153      valuesCopy = new SCIMAttributeValue[0];
154    }
155    return new SCIMAttribute(descriptor, valuesCopy);
156  }
157
158
159
160  /**
161   * Retrieve the name of the schema to which this attribute belongs.
162   *
163   * @return  The name of the schema to which this attribute belongs.
164   */
165  public String getSchema()
166  {
167    return this.attributeDescriptor.getSchema();
168  }
169
170
171
172  /**
173   * Retrieve the name of this attribute. The name does not indicate which
174   * schema the attribute belongs to.
175   *
176   * @return  The name of this attribute.
177   */
178  public String getName()
179  {
180    return this.attributeDescriptor.getName();
181  }
182
183
184
185  /**
186   * Retrieves the value of this attribute. This method should only be
187   * called if the attribute is single valued.
188   *
189   * @return  The value of this attribute.
190   */
191  public SCIMAttributeValue getValue()
192  {
193    return values[0];
194  }
195
196
197
198  /**
199   * Retrieves the values of this attribute. This method should only be
200   * called if the attribute is multi-valued.
201   *
202   * @return  The values of this attribute.
203   */
204  public SCIMAttributeValue[] getValues()
205  {
206    return values;
207  }
208
209  /**
210   * Retrieves the SCIM attribute mapping of this this attribute.
211   *
212   * @return The attribute descriptor
213   */
214  public AttributeDescriptor getAttributeDescriptor() {
215    return attributeDescriptor;
216  }
217
218
219
220  /**
221   * Determine whether this attribute matches the provided filter parameters.
222   *
223   * @param filter  The filter parameters to be compared against this attribute.
224   *
225   * @return  {@code true} if this attribute matches the provided filter, and
226   *          {@code false} otherwise.
227   */
228  public boolean matchesFilter(final SCIMFilter filter)
229  {
230    final SCIMFilterType type = filter.getFilterType();
231    final List<SCIMFilter> components = filter.getFilterComponents();
232
233    switch(type)
234    {
235      case AND:
236        for(SCIMFilter component : components)
237        {
238          if(!matchesFilter(component))
239          {
240            return false;
241          }
242        }
243        return true;
244      case OR:
245        for(SCIMFilter component : components)
246        {
247          if(matchesFilter(component))
248          {
249            return true;
250          }
251        }
252        return false;
253    }
254
255    final String schema = filter.getFilterAttribute().getAttributeSchema();
256    if (!schema.equalsIgnoreCase(getSchema()))
257    {
258      return false;
259    }
260
261    final String attributeName = filter.getFilterAttribute().getAttributeName();
262    String subAttributeName =
263        filter.getFilterAttribute().getSubAttributeName();
264    if (subAttributeName == null)
265    {
266      subAttributeName = "value";
267    }
268
269    if (!attributeName.equalsIgnoreCase(getName()))
270    {
271      return false;
272    }
273
274    if (attributeDescriptor.isMultiValued())
275    {
276      for (final SCIMAttributeValue v : getValues())
277      {
278        if (v.isComplex())
279        {
280          final Collection<AttributeDescriptor> descriptors =
281              attributeDescriptor.getSubAttributes();
282          for (AttributeDescriptor descriptor : descriptors)
283          {
284            final SCIMAttribute a = v.getAttribute(descriptor.getName());
285
286            if (a != null)
287            {
288              // This is done because the client specifies 'emails' rather
289              // than 'emails.email'.
290              final AttributePath childPath =
291                  new AttributePath(schema, a.getName(), subAttributeName);
292              if (a.matchesFilter(new SCIMFilter(type,
293                                                 childPath,
294                                                 filter.getFilterValue(),
295                                                 filter.isQuoteFilterValue(),
296                                                 filter.getFilterComponents())))
297              {
298                return true;
299              }
300            }
301          }
302        }
303        else
304        {
305          AttributeDescriptor singularDescriptor =
306                  AttributeDescriptor.createAttribute(getName(),
307                          attributeDescriptor.getDataType(),
308                          attributeDescriptor.getDescription(), getSchema(),
309                          attributeDescriptor.isReadOnly(),
310                          attributeDescriptor.isRequired(),
311                          attributeDescriptor.isCaseExact());
312
313          final SCIMAttribute singularAttr = create(singularDescriptor, v);
314          if (singularAttr.matchesFilter(filter))
315          {
316            return true;
317          }
318        }
319      }
320    }
321    else
322    {
323      final SCIMAttributeValue v = getValue();
324      if (v.isComplex())
325      {
326        if (subAttributeName != null)
327        {
328          final SCIMAttribute a = v.getAttribute(subAttributeName);
329          if (a != null)
330          {
331            final AttributePath childPath =
332                new AttributePath(schema, subAttributeName, null);
333            return a.matchesFilter(
334                new SCIMFilter(type,
335                               childPath,
336                               filter.getFilterValue(),
337                               filter.isQuoteFilterValue(),
338                               filter.getFilterComponents()));
339          }
340        }
341      }
342      else
343      {
344        if (type == SCIMFilterType.PRESENCE)
345        {
346          return true;
347        }
348
349        final AttributeDescriptor.DataType dataType =
350                  attributeDescriptor.getDataType();
351
352        String stringValue = null;
353        Double doubleValue = null;
354        Long longValue = null;
355        Date dateValue = null;
356        Boolean boolValue = null;
357        byte[] binValue = null;
358
359        switch(dataType)
360        {
361          case BINARY:
362            binValue = v.getBinaryValue();
363            if(binValue == null)
364            {
365              return false;
366            }
367            break;
368          case BOOLEAN:
369            boolValue = v.getBooleanValue();
370            if(boolValue == null)
371            {
372              return false;
373            }
374            break;
375          case DATETIME:
376            dateValue = v.getDateValue();
377            if(dateValue == null)
378            {
379              return false;
380            }
381            break;
382          case DECIMAL:
383            doubleValue = v.getDecimalValue();
384            if(doubleValue == null)
385            {
386              return false;
387            }
388            break;
389          case INTEGER:
390            longValue = v.getIntegerValue();
391            if(longValue == null)
392            {
393              return false;
394            }
395            break;
396          case STRING:
397            stringValue = v.getStringValue();
398            if(stringValue == null)
399            {
400              return false;
401            }
402            break;
403          default:
404            throw new RuntimeException(
405                    "Invalid attribute data type: " + dataType);
406        }
407
408        final String filterValue = filter.getFilterValue();
409
410        // TODO support caseExact attributes
411
412        //Note: The code below explicitly unboxes the objects before comparing
413        //      to avoid auto-unboxing and make it clear that it is just
414        //      primitives being compared.
415        switch (type)
416        {
417          case EQUALITY:
418            if(stringValue != null)
419            {
420              return stringValue.equalsIgnoreCase(filterValue);
421            }
422            else if(doubleValue != null)
423            {
424              try
425              {
426                double filterValueDouble = Double.parseDouble(filterValue);
427                return doubleValue.doubleValue() == filterValueDouble;
428              }
429              catch(NumberFormatException e)
430              {
431                return false;
432              }
433            }
434            else if(longValue != null)
435            {
436              try
437              {
438                long filterValueLong = Long.parseLong(filterValue);
439                return longValue.longValue() == filterValueLong;
440              }
441              catch(NumberFormatException e)
442              {
443                return false;
444              }
445            }
446            else if(boolValue != null)
447            {
448              return boolValue.booleanValue() ==
449                            Boolean.parseBoolean(filterValue);
450            }
451            else if(dateValue != null)
452            {
453              try
454              {
455                SimpleValue filterValueDate = new SimpleValue(filterValue);
456                return dateValue.equals(filterValueDate.getDateValue());
457              }
458              catch(IllegalArgumentException e)
459              {
460                return false;
461              }
462            }
463            else if(binValue != null)
464            {
465              //TODO: It's debatable whether this ought to just check whether
466              //      the base-64 encoded string is equal, rather than checking
467              //      if the bytes are equal. This seems more correct.
468              try
469              {
470                byte[] filterValueBytes =
471                            DatatypeConverter.parseBase64Binary(filterValue);
472                return Arrays.equals(binValue, filterValueBytes);
473              }
474              catch(IllegalArgumentException e)
475              {
476                return false;
477              }
478            }
479            return false;
480          case CONTAINS:
481            if(stringValue != null)
482            {
483              return StaticUtils.toLowerCase(stringValue).contains(
484                        StaticUtils.toLowerCase(filterValue));
485            }
486            else if(doubleValue != null)
487            {
488              try
489              {
490                double filterValueDouble = Double.parseDouble(filterValue);
491                return doubleValue.doubleValue() == filterValueDouble;
492              }
493              catch(NumberFormatException e)
494              {
495                return false;
496              }
497            }
498            else if(longValue != null)
499            {
500              try
501              {
502                long filterValueLong = Long.parseLong(filterValue);
503                return longValue.longValue() == filterValueLong;
504              }
505              catch(NumberFormatException e)
506              {
507                return false;
508              }
509            }
510            else if(boolValue != null)
511            {
512              return boolValue.booleanValue() ==
513                            Boolean.parseBoolean(filterValue);
514            }
515            else if(dateValue != null)
516            {
517              try
518              {
519                SimpleValue filterValueDate = new SimpleValue(filterValue);
520                return dateValue.equals(filterValueDate.getDateValue());
521              }
522              catch(IllegalArgumentException e)
523              {
524                return false;
525              }
526            }
527            else if(binValue != null)
528            {
529              try
530              {
531                byte[] filterValueBytes =
532                          DatatypeConverter.parseBase64Binary(filterValue);
533
534                boolean contains = false;
535                for(int i = 0; i < binValue.length; i++)
536                {
537                  if(binValue[i] == filterValueBytes[0])
538                  {
539                    contains = true;
540                    for(int j = 1; j < filterValueBytes.length; j++)
541                    {
542                      if(i+j >= binValue.length ||
543                          binValue[i+j] != filterValueBytes[j])
544                      {
545                        contains = false;
546                        break;
547                      }
548                    }
549
550                    if(contains)
551                    {
552                      break;
553                    }
554
555                  }
556                }
557
558                return contains;
559              }
560              catch(IllegalArgumentException e)
561              {
562                return false;
563              }
564            }
565            return false;
566          case STARTS_WITH:
567            if(stringValue != null)
568            {
569              return StaticUtils.toLowerCase(stringValue).startsWith(
570                        StaticUtils.toLowerCase(filterValue));
571            }
572            else if(doubleValue != null)
573            {
574              return false;
575            }
576            else if(longValue != null)
577            {
578              return false;
579            }
580            else if(boolValue != null)
581            {
582              return false;
583            }
584            else if(dateValue != null)
585            {
586              return false;
587            }
588            else if(binValue != null)
589            {
590              try
591              {
592                byte[] filterValueBytes =
593                          DatatypeConverter.parseBase64Binary(filterValue);
594
595                for(int i = 0; i < filterValueBytes.length; i++)
596                {
597                  if(binValue[i] != filterValueBytes[i])
598                  {
599                    return false;
600                  }
601                }
602                return true;
603              }
604              catch(IllegalArgumentException e)
605              {
606                return false;
607              }
608            }
609            return false;
610          case GREATER_THAN:
611            if(stringValue != null)
612            {
613              return stringValue.compareToIgnoreCase(filterValue) > 0;
614            }
615            else if(doubleValue != null)
616            {
617              try
618              {
619                double filterValueDouble = Double.parseDouble(filterValue);
620                return doubleValue.doubleValue() > filterValueDouble;
621              }
622              catch(NumberFormatException e)
623              {
624                return false;
625              }
626            }
627            else if(longValue != null)
628            {
629              try
630              {
631                long filterValueLong = Long.parseLong(filterValue);
632                return longValue.longValue() > filterValueLong;
633              }
634              catch(NumberFormatException e)
635              {
636                return false;
637              }
638            }
639            else if(boolValue != null)
640            {
641              return false;
642            }
643            else if(dateValue != null)
644            {
645              try
646              {
647                SimpleValue filterValueDate = new SimpleValue(filterValue);
648                return dateValue.after(filterValueDate.getDateValue());
649              }
650              catch(IllegalArgumentException e)
651              {
652                return false;
653              }
654            }
655            else if(binValue != null)
656            {
657              return false;
658            }
659            return false;
660          case GREATER_OR_EQUAL:
661            if(stringValue != null)
662            {
663              return stringValue.compareToIgnoreCase(filterValue) >= 0;
664            }
665            else if(doubleValue != null)
666            {
667              try
668              {
669                double filterValueDouble = Double.parseDouble(filterValue);
670                return doubleValue.doubleValue() >= filterValueDouble;
671              }
672              catch(NumberFormatException e)
673              {
674                return false;
675              }
676            }
677            else if(longValue != null)
678            {
679              try
680              {
681                long filterValueLong = Long.parseLong(filterValue);
682                return longValue.longValue() >= filterValueLong;
683              }
684              catch(NumberFormatException e)
685              {
686                return false;
687              }
688            }
689            else if(boolValue != null)
690            {
691              return false;
692            }
693            else if(dateValue != null)
694            {
695              try
696              {
697                SimpleValue filterValueDate = new SimpleValue(filterValue);
698                return dateValue.after(filterValueDate.getDateValue()) ||
699                         dateValue.equals(filterValueDate.getDateValue());
700              }
701              catch(IllegalArgumentException e)
702              {
703                return false;
704              }
705            }
706            else if(binValue != null)
707            {
708              return false;
709            }
710            return false;
711          case LESS_THAN:
712            if(stringValue != null)
713            {
714              return stringValue.compareToIgnoreCase(filterValue) < 0;
715            }
716            else if(doubleValue != null)
717            {
718              try
719              {
720                double filterValueDouble = Double.parseDouble(filterValue);
721                return doubleValue.doubleValue() < filterValueDouble;
722              }
723              catch(NumberFormatException e)
724              {
725                return false;
726              }
727            }
728            else if(longValue != null)
729            {
730              try
731              {
732                long filterValueLong = Long.parseLong(filterValue);
733                return longValue.longValue() < filterValueLong;
734              }
735              catch(NumberFormatException e)
736              {
737                return false;
738              }
739            }
740            else if(boolValue != null)
741            {
742              return false;
743            }
744            else if(dateValue != null)
745            {
746              try
747              {
748                SimpleValue filterValueDate = new SimpleValue(filterValue);
749                return dateValue.before(filterValueDate.getDateValue());
750              }
751              catch(IllegalArgumentException e)
752              {
753                return false;
754              }
755            }
756            else if(binValue != null)
757            {
758              return false;
759            }
760            return false;
761          case LESS_OR_EQUAL:
762            if(stringValue != null)
763            {
764              return stringValue.compareToIgnoreCase(filterValue) <= 0;
765            }
766            else if(doubleValue != null)
767            {
768              try
769              {
770                double filterValueDouble = Double.parseDouble(filterValue);
771                return doubleValue.doubleValue() <= filterValueDouble;
772              }
773              catch(NumberFormatException e)
774              {
775                return false;
776              }
777            }
778            else if(longValue != null)
779            {
780              try
781              {
782                long filterValueLong = Long.parseLong(filterValue);
783                return longValue.longValue() <= filterValueLong;
784              }
785              catch(NumberFormatException e)
786              {
787                return false;
788              }
789            }
790            else if(boolValue != null)
791            {
792              return false;
793            }
794            else if(dateValue != null)
795            {
796              try
797              {
798                SimpleValue filterValueDate = new SimpleValue(filterValue);
799                return dateValue.before(filterValueDate.getDateValue()) ||
800                         dateValue.equals(filterValueDate.getDateValue());
801              }
802              catch(IllegalArgumentException e)
803              {
804                return false;
805              }
806            }
807            else if(binValue != null)
808            {
809              return false;
810            }
811            return false;
812        }
813      }
814    }
815
816    return false;
817  }
818
819  @Override
820  public boolean equals(final Object o) {
821    if (this == o) {
822      return true;
823    }
824    if (o == null || getClass() != o.getClass()) {
825      return false;
826    }
827
828    SCIMAttribute that = (SCIMAttribute) o;
829
830    //Convert the value arrays into Sets so that the order of the attributes
831    //doesn't matter.
832    Set<SCIMAttributeValue> valueSet1 =
833            new HashSet<SCIMAttributeValue>(Arrays.asList(values));
834    Set<SCIMAttributeValue> valueSet2 =
835            new HashSet<SCIMAttributeValue>(Arrays.asList(that.values));
836
837    return attributeDescriptor.equals(that.attributeDescriptor) &&
838            valueSet1.equals(valueSet2);
839  }
840
841  @Override
842  public int hashCode() {
843    int result = attributeDescriptor.hashCode();
844    result = 31 * result + (values != null ?
845        Arrays.hashCode(values) : 0);
846    return result;
847  }
848
849  @Override
850  public String toString()
851  {
852    final StringBuilder sb = new StringBuilder();
853    sb.append("SCIMAttribute");
854    sb.append("{attribute=").append(attributeDescriptor.getSchema());
855    sb.append(SCIMConstants.SEPARATOR_CHAR_QUALIFIED_ATTRIBUTE);
856    sb.append(attributeDescriptor.getName());
857    sb.append(", values=").append(values == null ? "null" :
858        Arrays.asList(values).toString());
859    sb.append('}');
860    return sb.toString();
861  }
862}