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