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