001/*
002 * Copyright 2010-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2010-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2010-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk.controls;
037
038
039
040import java.util.ArrayList;
041
042import com.unboundid.asn1.ASN1Boolean;
043import com.unboundid.asn1.ASN1Constants;
044import com.unboundid.asn1.ASN1Element;
045import com.unboundid.asn1.ASN1Enumerated;
046import com.unboundid.asn1.ASN1OctetString;
047import com.unboundid.asn1.ASN1Sequence;
048import com.unboundid.ldap.sdk.Control;
049import com.unboundid.ldap.sdk.LDAPException;
050import com.unboundid.ldap.sdk.ResultCode;
051import com.unboundid.util.Debug;
052import com.unboundid.util.NotMutable;
053import com.unboundid.util.NotNull;
054import com.unboundid.util.Nullable;
055import com.unboundid.util.StaticUtils;
056import com.unboundid.util.ThreadSafety;
057import com.unboundid.util.ThreadSafetyLevel;
058import com.unboundid.util.Validator;
059
060import static com.unboundid.ldap.sdk.controls.ControlMessages.*;
061
062
063
064/**
065 * This class provides an implementation of the LDAP content synchronization
066 * request control as defined in
067 * <a href="http://www.ietf.org/rfc/rfc4533.txt">RFC 4533</a>.  It may be
068 * included in a search request to indicate that the client wishes to stay in
069 * sync with the server and/or be updated when server data changes.
070 * <BR><BR>
071 * Searches containing this control have the potential to take a very long time
072 * to complete (and may potentially never complete if the
073 * {@link ContentSyncRequestMode#REFRESH_AND_PERSIST} mode is selected), may
074 * return a large number of entries, and may also return intermediate response
075 * messages.  When using this control, it is important to keep the following in
076 * mind:
077 * <UL>
078 *   <LI>The associated search request should have a
079 *       {@link com.unboundid.ldap.sdk.SearchResultListener} so that entries
080 *       will be made available as soon as they are returned rather than having
081 *       to wait for the search to complete and/or consuming a large amount of
082 *       memory by storing the entries in a list that is only made available
083 *       when the search completes.  It may be desirable to use an
084 *       {@link com.unboundid.ldap.sdk.AsyncSearchResultListener} to perform the
085 *       search as an asynchronous operation so that the search request thread
086 *       does not block while waiting for the search to complete.</LI>
087 *   <LI>Entries and references returned from the search should include the
088 *       {@link ContentSyncStateControl} with the associated entryUUID and
089 *       potentially a cookie with an updated sync session state.  You should
090 *       call {@code getControl(ContentSyncStateControl.SYNC_STATE_OID)} on the
091 *       search result entries and references in order to retrieve the control
092 *       with the sync state information.</LI>
093 *   <LI>The search request should be configured with an unlimited server-side
094 *       time limit using {@code SearchRequest.setTimeLimitSeconds(0)}, and an
095 *       unlimited client-side timeout using
096 *       {@code SearchRequest.setResponseTimeoutMillis(0L)}.</LI>
097 *   <LI>The search request should be configured with an intermediate response
098 *       listener using the
099 *       {@code SearchRequest.setIntermediateResponseListener} method.</LI>
100 *   <LI>If the search does complete, then the
101 *       {@link com.unboundid.ldap.sdk.SearchResult} (or
102 *       {@link com.unboundid.ldap.sdk.LDAPSearchException} if the search ended
103 *       with a non-success response) may include a
104 *       {@link ContentSyncDoneControl} with updated sync state information.
105 *       You should call
106 *       {@code getResponseControl(ContentSyncDoneControl.SYNC_DONE_OID)} to
107 *       retrieve the control with the sync state information.</LI>
108 * </UL>
109 */
110@NotMutable()
111@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
112public final class ContentSyncRequestControl
113       extends Control
114{
115  /**
116   * The OID (1.3.6.1.4.1.4203.1.9.1.1) for the sync request control.
117   */
118  @NotNull public static final String SYNC_REQUEST_OID =
119       "1.3.6.1.4.1.4203.1.9.1.1";
120
121
122
123  /**
124   * The serial version UID for this serializable class.
125   */
126  private static final long serialVersionUID = -3183343423271667072L;
127
128
129
130  // The cookie to include in the sync request.
131  @Nullable private final ASN1OctetString cookie;
132
133  // Indicates whether to request an initial content in the event that the
134  // server determines that the client cannot reach convergence with the server
135  // data by continuing with incremental synchronization.
136  private final boolean reloadHint;
137
138  // The request mode for this control.
139  @NotNull private final ContentSyncRequestMode mode;
140
141
142
143  /**
144   * Creates a new content synchronization request control that will attempt to
145   * retrieve the initial content for the synchronization using the provided
146   * request mode.  It will be marked critical.
147   *
148   * @param  mode  The request mode which indicates whether to retrieve only
149   *               the initial content or to both retrieve the initial content
150   *               and be updated of changes made in the future.  It must not
151   *               be {@code null}.
152   */
153  public ContentSyncRequestControl(@NotNull final ContentSyncRequestMode mode)
154  {
155    this(true, mode, null, false);
156  }
157
158
159
160  /**
161   * Creates a new content synchronization request control that may be used to
162   * either retrieve the initial content or an incremental update.  It will be
163   * marked critical.  It will be marked critical.
164   *
165   * @param  mode        The request mode which indicates whether to retrieve
166   *                     only the initial content or to both retrieve the
167   *                     initial content and be updated of changes made in the
168   *                     future.  It must not be {@code null}.
169   * @param  cookie      A cookie providing state information for an existing
170   *                     synchronization session.  It may be {@code null} to
171   *                     perform an initial synchronization rather than an
172   *                     incremental update.
173   * @param  reloadHint  Indicates whether the client wishes to retrieve an
174   *                     initial content during an incremental update if the
175   *                     server determines that the client cannot reach
176   *                     convergence with the server data.
177   */
178  public ContentSyncRequestControl(@NotNull final ContentSyncRequestMode mode,
179                                   @Nullable final ASN1OctetString cookie,
180                                   final boolean reloadHint)
181  {
182    this(true, mode, cookie, reloadHint);
183  }
184
185
186
187  /**
188   * Creates a new content synchronization request control that may be used to
189   * either retrieve the initial content or an incremental update.
190   *
191   * @param  isCritical  Indicates whether this control should be marked
192   *                     critical.
193   * @param  mode        The request mode which indicates whether to retrieve
194   *                     only the initial content or to both retrieve the
195   *                     initial content and be updated of changes made in the
196   *                     future.  It must not be {@code null}.
197   * @param  cookie      A cookie providing state information for an existing
198   *                     synchronization session.  It may be {@code null} to
199   *                     perform an initial synchronization rather than an
200   *                     incremental update.
201   * @param  reloadHint  Indicates whether the client wishes to retrieve an
202   *                     initial content during an incremental update if the
203   *                     server determines that the client cannot reach
204   *                     convergence with the server data.
205   */
206  public ContentSyncRequestControl(final boolean isCritical,
207                                   @NotNull final ContentSyncRequestMode mode,
208                                   @Nullable final ASN1OctetString cookie,
209                                   final boolean reloadHint)
210  {
211    super(SYNC_REQUEST_OID, isCritical, encodeValue(mode, cookie, reloadHint));
212
213    this.mode       = mode;
214    this.cookie     = cookie;
215    this.reloadHint = reloadHint;
216  }
217
218
219
220  /**
221   * Creates a new content synchronization request control which is decoded from
222   * the provided generic control.
223   *
224   * @param  control  The generic control to be decoded as a content
225   *                  synchronization request control.
226   *
227   * @throws  LDAPException  If the provided control cannot be decoded as a
228   *                         content synchronization request control.
229   */
230  public ContentSyncRequestControl(@NotNull final Control control)
231         throws LDAPException
232  {
233    super(control);
234
235    final ASN1OctetString value = control.getValue();
236    if (value == null)
237    {
238      throw new LDAPException(ResultCode.DECODING_ERROR,
239           ERR_SYNC_REQUEST_NO_VALUE.get());
240    }
241
242    ASN1OctetString        c = null;
243    Boolean                h = null;
244    ContentSyncRequestMode m = null;
245
246    try
247    {
248      final ASN1Sequence s = ASN1Sequence.decodeAsSequence(value.getValue());
249      for (final ASN1Element e : s.elements())
250      {
251        switch (e.getType())
252        {
253          case ASN1Constants.UNIVERSAL_ENUMERATED_TYPE:
254            if (m != null)
255            {
256              throw new LDAPException(ResultCode.DECODING_ERROR,
257                   ERR_SYNC_REQUEST_VALUE_MULTIPLE_MODES.get());
258            }
259
260            final ASN1Enumerated modeElement =
261                 ASN1Enumerated.decodeAsEnumerated(e);
262            m = ContentSyncRequestMode.valueOf(modeElement.intValue());
263            if (m == null)
264            {
265              throw new LDAPException(ResultCode.DECODING_ERROR,
266                   ERR_SYNC_REQUEST_VALUE_INVALID_MODE.get(
267                        modeElement.intValue()));
268            }
269            break;
270
271          case ASN1Constants.UNIVERSAL_OCTET_STRING_TYPE:
272            if (c == null)
273            {
274              c = ASN1OctetString.decodeAsOctetString(e);
275            }
276            else
277            {
278              throw new LDAPException(ResultCode.DECODING_ERROR,
279                   ERR_SYNC_REQUEST_VALUE_MULTIPLE_COOKIES.get());
280            }
281            break;
282
283          case ASN1Constants.UNIVERSAL_BOOLEAN_TYPE:
284            if (h == null)
285            {
286              h = ASN1Boolean.decodeAsBoolean(e).booleanValue();
287            }
288            else
289            {
290              throw new LDAPException(ResultCode.DECODING_ERROR,
291                   ERR_SYNC_REQUEST_VALUE_MULTIPLE_HINTS.get());
292            }
293            break;
294
295          default:
296            throw new LDAPException(ResultCode.DECODING_ERROR,
297                 ERR_SYNC_REQUEST_VALUE_INVALID_ELEMENT_TYPE.get(
298                      StaticUtils.toHex(e.getType())));
299        }
300      }
301    }
302    catch (final LDAPException le)
303    {
304      throw le;
305    }
306    catch (final Exception e)
307    {
308      Debug.debugException(e);
309
310      throw new LDAPException(ResultCode.DECODING_ERROR,
311           ERR_SYNC_REQUEST_VALUE_CANNOT_DECODE.get(
312                StaticUtils.getExceptionMessage(e)), e);
313    }
314
315    if (m == null)
316    {
317      throw new LDAPException(ResultCode.DECODING_ERROR,
318           ERR_SYNC_REQUEST_VALUE_NO_MODE.get());
319    }
320    else
321    {
322      mode = m;
323    }
324
325    if (h == null)
326    {
327      reloadHint = false;
328    }
329    else
330    {
331      reloadHint = h;
332    }
333
334    cookie = c;
335  }
336
337
338
339  /**
340   * Encodes the provided information into a form suitable for use as the value
341   * of this control.
342   *
343   * @param  mode        The request mode which indicates whether to retrieve
344   *                     only the initial content or to both retrieve the
345   *                     initial content and be updated of changes made in the
346   *                     future.  It must not be {@code null}.
347   * @param  cookie      A cookie providing state information for an existing
348   *                     synchronization session.  It may be {@code null} to
349   *                     perform an initial synchronization rather than an
350   *                     incremental update.
351   * @param  reloadHint  Indicates whether the client wishes to retrieve an
352   *                     initial content during an incremental update if the
353   *                     server determines that the client cannot reach
354   *                     convergence with the server data.
355   *
356   * @return  An ASN.1 octet string containing the encoded control value.
357   */
358  @NotNull()
359  private static ASN1OctetString encodeValue(
360                      @NotNull final ContentSyncRequestMode mode,
361                      @Nullable final ASN1OctetString cookie,
362                      final boolean reloadHint)
363  {
364    Validator.ensureNotNull(mode);
365
366    final ArrayList<ASN1Element> elements = new ArrayList<>(3);
367    elements.add(new ASN1Enumerated(mode.intValue()));
368
369    if (cookie != null)
370    {
371      elements.add(cookie);
372    }
373
374    if (reloadHint)
375    {
376      elements.add(ASN1Boolean.UNIVERSAL_BOOLEAN_TRUE_ELEMENT);
377    }
378
379    return new ASN1OctetString(new ASN1Sequence(elements).encode());
380  }
381
382
383
384  /**
385   * Retrieves the mode for this content synchronization request control, which
386   * indicates whether to retrieve an initial content or an incremental update.
387   *
388   * @return  The mode for this content synchronization request control.
389   */
390  @NotNull()
391  public ContentSyncRequestMode getMode()
392  {
393    return mode;
394  }
395
396
397
398  /**
399   * Retrieves a cookie providing state information for an existing
400   * synchronization session, if available.
401   *
402   * @return  A cookie providing state information for an existing
403   *          synchronization session, or {@code null} if none is available and
404   *          an initial content should be retrieved.
405   */
406  @Nullable()
407  public ASN1OctetString getCookie()
408  {
409    return cookie;
410  }
411
412
413
414  /**
415   * Retrieves the reload hint value for this synchronization request control.
416   *
417   * @return  {@code true} if the server should return an initial content rather
418   *          than an incremental update if it determines that the client cannot
419   *          reach convergence, or {@code false} if it should return an
420   *          e-sync refresh required result in that case.
421   */
422  public boolean getReloadHint()
423  {
424    return reloadHint;
425  }
426
427
428
429  /**
430   * {@inheritDoc}
431   */
432  @Override()
433  @NotNull()
434  public String getControlName()
435  {
436    return INFO_CONTROL_NAME_CONTENT_SYNC_REQUEST.get();
437  }
438
439
440
441  /**
442   * {@inheritDoc}
443   */
444  @Override()
445  public void toString(@NotNull final StringBuilder buffer)
446  {
447    buffer.append("ContentSyncRequestControl(mode='");
448    buffer.append(mode.name());
449    buffer.append('\'');
450
451    if (cookie != null)
452    {
453      buffer.append(", cookie='");
454      StaticUtils.toHex(cookie.getValue(), buffer);
455      buffer.append('\'');
456    }
457
458    buffer.append(", reloadHint=");
459    buffer.append(reloadHint);
460    buffer.append(')');
461  }
462}