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