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