001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2008-2009 Sun Microsystems, Inc.
025 *      Portions Copyright 2014-2015 ForgeRock AS
026 */
027package org.opends.server.controls;
028
029import org.forgerock.i18n.LocalizableMessage;
030
031import java.util.ArrayList;
032import java.util.StringTokenizer;
033import java.io.IOException;
034
035import org.forgerock.opendj.ldap.schema.MatchingRule;
036import org.opends.server.core.DirectoryServer;
037import org.forgerock.opendj.io.*;
038import org.opends.server.protocols.ldap.LDAPResultCode;
039import org.opends.server.types.*;
040import org.forgerock.opendj.ldap.ResultCode;
041import org.forgerock.opendj.ldap.ByteString;
042import static org.opends.messages.ProtocolMessages.*;
043import static org.opends.server.util.ServerConstants.*;
044import static org.opends.server.util.StaticUtils.*;
045
046/**
047 * This class implements the server-side sort request control as defined in RFC
048 * 2891 section 1.1. The subclass ServerSideSortRequestControl.ClientRequest
049 * should be used when encoding this control from a sort order string. This is
050 * suitable for client tools that want to encode this control without a
051 * SortOrder object. The ASN.1 description for the control value is:
052 * <BR><BR>
053 * <PRE>
054 * SortKeyList ::= SEQUENCE OF SEQUENCE {
055 *            attributeType   AttributeDescription,
056 *            orderingRule    [0] MatchingRuleId OPTIONAL,
057 *            reverseOrder    [1] BOOLEAN DEFAULT FALSE }
058 * </PRE>
059 */
060public class ServerSideSortRequestControl
061    extends Control
062{
063  /**
064   * The BER type to use when encoding the orderingRule element.
065   */
066  private static final byte TYPE_ORDERING_RULE_ID = (byte) 0x80;
067
068
069
070  /**
071   * The BER type to use when encoding the reverseOrder element.
072   */
073  private static final byte TYPE_REVERSE_ORDER = (byte) 0x81;
074
075
076  /**
077   * ControlDecoder implementation to decode this control from a ByteString.
078   */
079  private static final class Decoder
080      implements ControlDecoder<ServerSideSortRequestControl>
081  {
082    /** {@inheritDoc} */
083    public ServerSideSortRequestControl decode(boolean isCritical,
084                                               ByteString value)
085        throws DirectoryException
086    {
087      if (value == null)
088      {
089        LocalizableMessage message = INFO_SORTREQ_CONTROL_NO_VALUE.get();
090        throw new DirectoryException(ResultCode.PROTOCOL_ERROR, message);
091      }
092
093      ASN1Reader reader = ASN1.getReader(value);
094      try
095      {
096        reader.readStartSequence();
097        if (!reader.hasNextElement())
098        {
099          LocalizableMessage message = INFO_SORTREQ_CONTROL_NO_SORT_KEYS.get();
100          throw new DirectoryException(ResultCode.PROTOCOL_ERROR, message);
101        }
102
103        ArrayList<SortKey> sortKeys = new ArrayList<>();
104        while(reader.hasNextElement())
105        {
106          reader.readStartSequence();
107          String attrName = toLowerCase(reader.readOctetStringAsString());
108          AttributeType attrType = DirectoryServer.getAttributeTypeOrNull(attrName);
109          if (attrType == null)
110          {
111            //This attribute is not defined in the schema. There is no point
112            //iterating over the next attribute and return a partially sorted result.
113            return new ServerSideSortRequestControl(isCritical,
114            new SortOrder(sortKeys.toArray(new SortKey[0])));
115          }
116
117          MatchingRule orderingRule = null;
118          boolean ascending = true;
119          if(reader.hasNextElement() &&
120              reader.peekType() == TYPE_ORDERING_RULE_ID)
121          {
122            String orderingRuleID =
123                toLowerCase(reader.readOctetStringAsString());
124            orderingRule =
125                DirectoryServer.getMatchingRule(orderingRuleID);
126            if (orderingRule == null)
127            {
128              LocalizableMessage message =
129                  INFO_SORTREQ_CONTROL_UNDEFINED_ORDERING_RULE.
130                      get(orderingRuleID);
131              throw new DirectoryException(ResultCode.PROTOCOL_ERROR,
132                  message);
133            }
134          }
135          if(reader.hasNextElement() &&
136              reader.peekType() == TYPE_REVERSE_ORDER)
137          {
138            ascending = ! reader.readBoolean();
139          }
140          reader.readEndSequence();
141
142          if (orderingRule == null && attrType.getOrderingMatchingRule() == null)
143          {
144            LocalizableMessage message =
145                INFO_SORTREQ_CONTROL_NO_ORDERING_RULE_FOR_ATTR.get(attrName);
146            throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
147          }
148
149          sortKeys.add(new SortKey(attrType, ascending, orderingRule));
150        }
151        reader.readEndSequence();
152
153        return new ServerSideSortRequestControl(isCritical,
154            new SortOrder(sortKeys.toArray(new SortKey[0])));
155      }
156      catch (DirectoryException de)
157      {
158        throw de;
159      }
160      catch (Exception e)
161      {
162        LocalizableMessage message =
163            INFO_SORTREQ_CONTROL_CANNOT_DECODE_VALUE.get(
164                getExceptionMessage(e));
165        throw new DirectoryException(ResultCode.PROTOCOL_ERROR, message, e);
166      }
167    }
168
169    public String getOID()
170    {
171      return OID_SERVER_SIDE_SORT_REQUEST_CONTROL;
172    }
173
174  }
175
176  /**
177   * The Control Decoder that can be used to decode this control.
178   */
179  public static final ControlDecoder<ServerSideSortRequestControl> DECODER =
180      new Decoder();
181
182  /** The sort order associated with this control represented by strings. */
183  private ArrayList<String[]> decodedKeyList;
184
185  /** The sort order associated with this control. */
186  private SortOrder sortOrder;
187
188  /**
189   * Creates a new server-side sort request control based on the definition in
190   * the provided sort order string.
191   *
192   * @param  sortOrderString  The string representation of the sort order to
193   *                          use for the control.
194   * @throws LDAPException If the provided sort order string could not be
195   *                       decoded.
196   */
197  public ServerSideSortRequestControl(String sortOrderString)
198      throws LDAPException
199  {
200    this(false, sortOrderString);
201  }
202
203  /**
204   * Creates a new server-side sort request control based on the definition in
205   * the provided sort order string.
206   *
207   * @param  isCritical    Indicates whether support for this control
208   *                       should be considered a critical part of the
209   *                       server processing.
210   * @param  sortOrderString  The string representation of the sort order to
211   *                          use for the control.
212   * @throws LDAPException If the provided sort order string could not be
213   *                       decoded.
214   */
215  public ServerSideSortRequestControl(boolean isCritical,
216                                      String sortOrderString)
217      throws LDAPException
218  {
219    super(OID_SERVER_SIDE_SORT_REQUEST_CONTROL, isCritical);
220
221    StringTokenizer tokenizer = new StringTokenizer(sortOrderString, ",");
222
223    decodedKeyList = new ArrayList<>();
224    while (tokenizer.hasMoreTokens())
225    {
226      String token = tokenizer.nextToken().trim();
227      boolean reverseOrder = false;
228      if (token.startsWith("-"))
229      {
230        reverseOrder = true;
231        token = token.substring(1);
232      }
233      else if (token.startsWith("+"))
234      {
235        token = token.substring(1);
236      }
237
238      int colonPos = token.indexOf(':');
239      if (colonPos < 0)
240      {
241        if (token.length() == 0)
242        {
243          LocalizableMessage message =
244              INFO_SORTREQ_CONTROL_NO_ATTR_NAME.get(sortOrderString);
245          throw new LDAPException(LDAPResultCode.PROTOCOL_ERROR, message);
246        }
247
248        if (reverseOrder)
249        {
250          decodedKeyList.add(new String[]{token, null, "r"});
251        }
252        else
253        {
254          decodedKeyList.add(new String[]{token, null, null});
255        }
256      }
257      else if (colonPos == 0)
258      {
259        LocalizableMessage message =
260            INFO_SORTREQ_CONTROL_NO_ATTR_NAME.get(sortOrderString);
261        throw new LDAPException(LDAPResultCode.PROTOCOL_ERROR, message);
262      }
263      else if (colonPos == (token.length() - 1))
264      {
265        LocalizableMessage message =
266            INFO_SORTREQ_CONTROL_NO_MATCHING_RULE.get(sortOrderString);
267        throw new LDAPException(LDAPResultCode.PROTOCOL_ERROR, message);
268      }
269      else
270      {
271        String attrName = token.substring(0, colonPos);
272        String ruleID   = token.substring(colonPos+1);
273
274        if (reverseOrder)
275        {
276          decodedKeyList.add(new String[]{attrName, ruleID, "r"});
277        }
278        else
279        {
280          decodedKeyList.add(new String[]{attrName, ruleID, null});
281        }
282      }
283    }
284
285    if (decodedKeyList.isEmpty())
286    {
287      LocalizableMessage message = INFO_SORTREQ_CONTROL_NO_SORT_KEYS.get();
288      throw new LDAPException(LDAPResultCode.PROTOCOL_ERROR, message);
289    }
290  }
291
292
293  /**
294   * Creates a new server-side sort request control based on the provided sort
295   * order.
296   *
297   * @param  sortOrder  The sort order to use for this control.
298   */
299  public ServerSideSortRequestControl(SortOrder sortOrder)
300  {
301    this(false, sortOrder);
302  }
303
304  /**
305   * Creates a new server-side sort request control with the provided
306   * information.
307   *
308   * @param  isCritical    Indicates whether support for this control should be
309   *                       considered a critical part of the server processing.
310   * @param  sortOrder     sort order associated with this server-side sort
311   *                       control.
312   */
313  public ServerSideSortRequestControl(boolean isCritical, SortOrder sortOrder)
314  {
315    super(OID_SERVER_SIDE_SORT_REQUEST_CONTROL, isCritical);
316
317    this.sortOrder = sortOrder;
318  }
319
320
321  /**
322   * Retrieves the sort order for this server-side sort request control.
323   *
324   * @return  The sort order for this server-side sort request control.
325   * @throws  DirectoryException if an error occurs while retriving the
326   *          sort order.
327   */
328  public SortOrder getSortOrder() throws DirectoryException
329  {
330    if(sortOrder == null)
331    {
332      sortOrder = decodeSortOrderFromString();
333    }
334
335    return sortOrder;
336  }
337
338  /**
339   * Indicates whether the sort control contains Sort keys.
340   *
341   * <P> A Sort control may not contain sort keys if the attribute type
342   * is not recognized by the server </P>
343   *
344   * @return  <CODE>true</CODE> if the control contains sort keys
345   *          or <CODE>false</CODE> if it does not.
346   *
347   * @throws  DirectoryException  If a problem occurs while trying to make the
348   *                              determination.
349   */
350  public boolean  containsSortKeys() throws DirectoryException
351  {
352    return getSortOrder().getSortKeys().length!=0;
353  }
354
355  /**
356   * Writes this control's value to an ASN.1 writer. The value (if any) must
357   * be written as an ASN1OctetString.
358   *
359   * @param writer The ASN.1 writer to use.
360   * @throws IOException If a problem occurs while writing to the stream.
361
362   */
363  @Override
364  protected void writeValue(ASN1Writer writer) throws IOException {
365    if(decodedKeyList != null)
366    {
367      // This control was created with a sort order string so encode using
368      // that.
369      writeValueFromString(writer);
370    }
371    else
372    {
373      // This control must have been created with a typed sort order object
374      // so encode using that.
375      writeValueFromSortOrder(writer);
376    }
377  }
378
379  /**
380   * Appends a string representation of this server-side sort request control
381   * to the provided buffer.
382   *
383   * @param  buffer  The buffer to which the information should be appended.
384   */
385  @Override
386  public void toString(StringBuilder buffer)
387  {
388    buffer.append("ServerSideSortRequestControl(");
389    if(sortOrder == null)
390    {
391      buffer.append("SortOrder(");
392
393      if (!decodedKeyList.isEmpty())
394      {
395        decodedKeyToString(decodedKeyList.get(0), buffer);
396
397        for (int i=1; i < decodedKeyList.size(); i++)
398        {
399          buffer.append(",");
400          decodedKeyToString(decodedKeyList.get(i), buffer);
401        }
402      }
403      buffer.append(")");
404    }
405    else
406    {
407      buffer.append(sortOrder);
408    }
409    buffer.append(")");
410  }
411
412  private void decodedKeyToString(String[] decodedKey, StringBuilder buffer)
413  {
414    buffer.append("SortKey(");
415    if (decodedKey[2] == null)
416    {
417      buffer.append("+");
418    }
419    else
420    {
421      buffer.append("-");
422    }
423    buffer.append(decodedKey[0]);
424
425    if (decodedKey[1] != null)
426    {
427      buffer.append(":");
428      buffer.append(decodedKey[1]);
429    }
430
431    buffer.append(")");
432  }
433
434  private SortOrder decodeSortOrderFromString() throws DirectoryException
435  {
436    ArrayList<SortKey> sortKeys = new ArrayList<>();
437    for(String[] decodedKey : decodedKeyList)
438    {
439      AttributeType attrType = DirectoryServer.getAttributeTypeOrNull(decodedKey[0].toLowerCase());
440      if (attrType == null)
441      {
442        //This attribute is not defined in the schema. There is no point
443        //iterating over the next attribute and return a partially sorted result.
444        return new SortOrder(sortKeys.toArray(new SortKey[0]));
445      }
446
447      MatchingRule orderingRule = null;
448      if(decodedKey[1] != null)
449      {
450        orderingRule = DirectoryServer.getMatchingRule(decodedKey[1].toLowerCase());
451        if (orderingRule == null)
452        {
453          LocalizableMessage message = INFO_SORTREQ_CONTROL_UNDEFINED_ORDERING_RULE.get(decodedKey[1]);
454          throw new DirectoryException(ResultCode.PROTOCOL_ERROR, message);
455        }
456      }
457
458      String decodedKey2 = decodedKey[2];
459      boolean ascending = decodedKey2 == null || !decodedKey2.equals("r");
460      if (orderingRule == null
461          && attrType.getOrderingMatchingRule() == null)
462      {
463        throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
464            INFO_SORTREQ_CONTROL_NO_ORDERING_RULE_FOR_ATTR.get(decodedKey[0]));
465      }
466
467      sortKeys.add(new SortKey(attrType, ascending, orderingRule));
468    }
469
470    return new SortOrder(sortKeys.toArray(new SortKey[0]));
471  }
472
473  private void writeValueFromString(ASN1Writer writer) throws IOException
474  {
475    writer.writeStartSequence(ASN1.UNIVERSAL_OCTET_STRING_TYPE);
476
477    writer.writeStartSequence();
478    for(String[] strs : decodedKeyList)
479    {
480      writer.writeStartSequence();
481      // Attr name will always be present
482      writer.writeOctetString(strs[0]);
483      // Rule ID might not be present
484      if(strs[1] != null)
485      {
486        writer.writeOctetString(TYPE_ORDERING_RULE_ID, strs[1]);
487      }
488      // Reverse if present
489      if(strs[2] != null)
490      {
491        writer.writeBoolean(TYPE_REVERSE_ORDER, true);
492      }
493      writer.writeEndSequence();
494    }
495    writer.writeEndSequence();
496
497    writer.writeEndSequence();
498  }
499
500  private void writeValueFromSortOrder(ASN1Writer writer) throws IOException
501  {
502    writer.writeStartSequence(ASN1.UNIVERSAL_OCTET_STRING_TYPE);
503
504    writer.writeStartSequence();
505    for (SortKey sortKey : sortOrder.getSortKeys())
506    {
507      writer.writeStartSequence();
508      writer.writeOctetString(sortKey.getAttributeType().getNameOrOID());
509
510      if (sortKey.getOrderingRule() != null)
511      {
512        writer.writeOctetString(TYPE_ORDERING_RULE_ID,
513            sortKey.getOrderingRule().getNameOrOID());
514      }
515
516      if (! sortKey.ascending())
517      {
518        writer.writeBoolean(TYPE_REVERSE_ORDER, true);
519      }
520
521      writer.writeEndSequence();
522    }
523    writer.writeEndSequence();
524
525    writer.writeEndSequence();
526  }
527}
528