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.text.ParseException;
041import java.util.ArrayList;
042import java.util.Collections;
043import java.util.Iterator;
044import java.util.List;
045import java.util.UUID;
046
047import com.unboundid.asn1.ASN1Boolean;
048import com.unboundid.asn1.ASN1Constants;
049import com.unboundid.asn1.ASN1Element;
050import com.unboundid.asn1.ASN1OctetString;
051import com.unboundid.asn1.ASN1Sequence;
052import com.unboundid.asn1.ASN1Set;
053import com.unboundid.ldap.sdk.Control;
054import com.unboundid.ldap.sdk.IntermediateResponse;
055import com.unboundid.ldap.sdk.LDAPException;
056import com.unboundid.ldap.sdk.ResultCode;
057import com.unboundid.util.Debug;
058import com.unboundid.util.NotMutable;
059import com.unboundid.util.NotNull;
060import com.unboundid.util.Nullable;
061import com.unboundid.util.StaticUtils;
062import com.unboundid.util.ThreadSafety;
063import com.unboundid.util.ThreadSafetyLevel;
064import com.unboundid.util.Validator;
065
066import static com.unboundid.ldap.sdk.controls.ControlMessages.*;
067
068
069
070/**
071 * This class provides an implementation of the sync info message, which is
072 * an intermediate response message used by the content synchronization
073 * operation as defined in
074 * <a href="http://www.ietf.org/rfc/rfc4533.txt">RFC 4533</a>.  Directory
075 * servers may return this response in the course of processing a search
076 * request containing the content synchronization request control.  See the
077 * documentation for the {@link ContentSyncRequestControl} class for more
078 * information about using the content synchronization operation.
079 */
080@NotMutable()
081@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
082public final class ContentSyncInfoIntermediateResponse
083       extends IntermediateResponse
084{
085  /**
086   * The OID (1.3.6.1.4.1.4203.1.9.1.4) for the sync info intermediate response.
087   */
088  @NotNull public static final String SYNC_INFO_OID =
089       "1.3.6.1.4.1.4203.1.9.1.4";
090
091
092
093  /**
094   * The serial version UID for this serializable class.
095   */
096  private static final long serialVersionUID = 4464376009337157433L;
097
098
099
100  // An updated state cookie, if available.
101  @Nullable private final ASN1OctetString cookie;
102
103  // Indicates whether the provided set of UUIDs represent entries that have
104  // been removed.
105  private final boolean refreshDeletes;
106
107  // Indicates whether the refresh phase is complete.
108  private final boolean refreshDone;
109
110  // The type of content synchronization information represented in this
111  // response.
112  @NotNull private final ContentSyncInfoType type;
113
114  // A list of entryUUIDs for the set of entries associated with this message.
115  @Nullable private final List<UUID> entryUUIDs;
116
117
118
119  /**
120   * Creates a new content synchronization info intermediate response with the
121   * provided information.
122   *
123   * @param  type            The type of content synchronization information
124   *                         represented in this response.
125   * @param  value           The encoded value for the intermediate response, if
126   *                         any.
127   * @param  cookie          An updated state cookie for the synchronization
128   *                         session, if available.
129   * @param  refreshDone     Indicates whether the refresh phase of the
130   *                         synchronization session is complete.
131   * @param  refreshDeletes  Indicates whether the provided set of UUIDs
132   *                         represent entries that have been removed.
133   * @param  entryUUIDs      A list of entryUUIDs for the set of entries
134   *                         associated with this message.
135   * @param  controls        The set of controls to include in the intermediate
136   *                         response, if any.
137   */
138  private ContentSyncInfoIntermediateResponse(
139                 @NotNull final ContentSyncInfoType type,
140                 @Nullable final ASN1OctetString value,
141                 @Nullable final ASN1OctetString cookie,
142                 final boolean refreshDone, final boolean refreshDeletes,
143                 @Nullable final List<UUID> entryUUIDs,
144                 @Nullable final Control... controls)
145  {
146    super(SYNC_INFO_OID, value, controls);
147
148    this.type           = type;
149    this.cookie         = cookie;
150    this.refreshDone    = refreshDone;
151    this.refreshDeletes = refreshDeletes;
152    this.entryUUIDs     = entryUUIDs;
153  }
154
155
156
157  /**
158   * Creates a new sync info intermediate response with a type of
159   * {@link ContentSyncInfoType#NEW_COOKIE}.
160   *
161   * @param  cookie    The updated state cookie for the synchronization session.
162   *                   It must not be {@code null}.
163   * @param  controls  An optional set of controls to include in the response.
164   *                   It may be {@code null} or empty if no controls should be
165   *                   included.
166   *
167   * @return  The created sync info intermediate response.
168   */
169  @NotNull()
170  public static ContentSyncInfoIntermediateResponse createNewCookieResponse(
171                     @NotNull final ASN1OctetString cookie,
172                     @Nullable final Control... controls)
173  {
174    Validator.ensureNotNull(cookie);
175
176    final ContentSyncInfoType type = ContentSyncInfoType.NEW_COOKIE;
177
178    return new ContentSyncInfoIntermediateResponse(type,
179         encodeValue(type, cookie, false, null, false),
180         cookie, false, false, null, controls);
181  }
182
183
184
185  /**
186   * Creates a new sync info intermediate response with a type of
187   * {@link ContentSyncInfoType#REFRESH_DELETE}.
188   *
189   * @param  cookie       The updated state cookie for the synchronization
190   *                      session.  It may be {@code null} if no new cookie is
191   *                      available.
192   * @param  refreshDone  Indicates whether the refresh phase of the
193   *                      synchronization operation has completed.
194   * @param  controls     An optional set of controls to include in the
195   *                      response.  It may be {@code null} or empty if no
196   *                      controls should be included.
197   *
198   * @return  The created sync info intermediate response.
199   */
200  @NotNull()
201  public static ContentSyncInfoIntermediateResponse createRefreshDeleteResponse(
202                     @Nullable final ASN1OctetString cookie,
203                     final boolean refreshDone,
204                     @Nullable final Control... controls)
205  {
206    final ContentSyncInfoType type = ContentSyncInfoType.REFRESH_DELETE;
207
208    return new ContentSyncInfoIntermediateResponse(type,
209         encodeValue(type, cookie, refreshDone, null, false),
210         cookie, refreshDone, false, null, controls);
211  }
212
213
214
215  /**
216   * Creates a new sync info intermediate response with a type of
217   * {@link ContentSyncInfoType#REFRESH_PRESENT}.
218   *
219   * @param  cookie       The updated state cookie for the synchronization
220   *                      session.  It may be {@code null} if no new cookie is
221   *                      available.
222   * @param  refreshDone  Indicates whether the refresh phase of the
223   *                      synchronization operation has completed.
224   * @param  controls     An optional set of controls to include in the
225   *                      response.  It may be {@code null} or empty if no
226   *                      controls should be included.
227   *
228   * @return  The created sync info intermediate response.
229   */
230  @NotNull()
231  public static ContentSyncInfoIntermediateResponse
232                     createRefreshPresentResponse(
233                          @Nullable final ASN1OctetString cookie,
234                          final boolean refreshDone,
235                          @Nullable final Control... controls)
236  {
237    final ContentSyncInfoType type = ContentSyncInfoType.REFRESH_PRESENT;
238
239    return new ContentSyncInfoIntermediateResponse(type,
240         encodeValue(type, cookie, refreshDone, null, false),
241         cookie, refreshDone, false, null, controls);
242  }
243
244
245
246  /**
247   * Creates a new sync info intermediate response with a type of
248   * {@link ContentSyncInfoType#SYNC_ID_SET}.
249   *
250   * @param  cookie          The updated state cookie for the synchronization
251   *                         session.  It may be {@code null} if no new cookie
252   *                         is available.
253   * @param  entryUUIDs      The set of entryUUIDs for the entries referenced in
254   *                         this response.  It must not be {@code null}.
255   * @param  refreshDeletes  Indicates whether the entryUUIDs represent entries
256   *                         that have been removed rather than those that have
257   *                         remained unchanged.
258   * @param  controls        An optional set of controls to include in the
259   *                         response.  It may be {@code null} or empty if no
260   *                         controls should be included.
261   *
262   * @return  The created sync info intermediate response.
263   */
264  @NotNull()
265  public static ContentSyncInfoIntermediateResponse createSyncIDSetResponse(
266                     @Nullable final ASN1OctetString cookie,
267                     @NotNull final List<UUID> entryUUIDs,
268                     final boolean refreshDeletes,
269                     @Nullable final Control... controls)
270  {
271    Validator.ensureNotNull(entryUUIDs);
272
273    final ContentSyncInfoType type = ContentSyncInfoType.SYNC_ID_SET;
274
275    return new ContentSyncInfoIntermediateResponse(type,
276         encodeValue(type, cookie, false, entryUUIDs, refreshDeletes),
277         cookie, false, refreshDeletes,
278         Collections.unmodifiableList(entryUUIDs), controls);
279  }
280
281
282
283  /**
284   * Decodes the provided generic intermediate response as a sync info
285   * intermediate response.
286   *
287   * @param  r  The intermediate response to be decoded as a sync info
288   *            intermediate response.  It must not be {@code null}.
289   *
290   * @return  The decoded sync info intermediate response.
291   *
292   * @throws  LDAPException  If a problem occurs while trying to decode the
293   *                         provided intermediate response as a sync info
294   *                         response.
295   */
296  @NotNull()
297  public static ContentSyncInfoIntermediateResponse decode(
298                     @NotNull final IntermediateResponse r)
299         throws LDAPException
300  {
301    final ASN1OctetString value = r.getValue();
302    if (value == null)
303    {
304      throw new LDAPException(ResultCode.DECODING_ERROR,
305           ERR_SYNC_INFO_IR_NO_VALUE.get());
306    }
307
308    final ASN1Element valueElement;
309    try
310    {
311      valueElement = ASN1Element.decode(value.getValue());
312    }
313    catch (final Exception e)
314    {
315      Debug.debugException(e);
316
317      throw new LDAPException(ResultCode.DECODING_ERROR,
318           ERR_SYNC_INFO_IR_VALUE_NOT_ELEMENT.get(
319                StaticUtils.getExceptionMessage(e)), e);
320    }
321
322    final ContentSyncInfoType type =
323         ContentSyncInfoType.valueOf(valueElement.getType());
324    if (type == null)
325    {
326      throw new LDAPException(ResultCode.DECODING_ERROR,
327           ERR_SYNC_INFO_IR_VALUE_UNRECOGNIZED_TYPE.get(
328                StaticUtils.toHex(valueElement.getType())));
329    }
330
331    ASN1OctetString cookie         = null;
332    boolean         refreshDone    = false;
333    boolean         refreshDeletes = false;
334    List<UUID>      entryUUIDs     = null;
335
336    try
337    {
338      switch (type)
339      {
340        case NEW_COOKIE:
341          cookie = new ASN1OctetString(valueElement.getValue());
342          break;
343
344        case REFRESH_DELETE:
345        case REFRESH_PRESENT:
346          refreshDone = true;
347
348          ASN1Sequence s = valueElement.decodeAsSequence();
349          for (final ASN1Element e : s.elements())
350          {
351            switch (e.getType())
352            {
353              case ASN1Constants.UNIVERSAL_OCTET_STRING_TYPE:
354                cookie = ASN1OctetString.decodeAsOctetString(e);
355                break;
356              case ASN1Constants.UNIVERSAL_BOOLEAN_TYPE:
357                refreshDone = ASN1Boolean.decodeAsBoolean(e).booleanValue();
358                break;
359              default:
360                throw new LDAPException(ResultCode.DECODING_ERROR,
361                     ERR_SYNC_INFO_IR_VALUE_INVALID_SEQUENCE_TYPE.get(
362                          type.name(), StaticUtils.toHex(e.getType())));
363            }
364          }
365          break;
366
367        case SYNC_ID_SET:
368          s = valueElement.decodeAsSequence();
369          for (final ASN1Element e : s.elements())
370          {
371            switch (e.getType())
372            {
373              case ASN1Constants.UNIVERSAL_OCTET_STRING_TYPE:
374                cookie = ASN1OctetString.decodeAsOctetString(e);
375                break;
376              case ASN1Constants.UNIVERSAL_BOOLEAN_TYPE:
377                refreshDeletes = ASN1Boolean.decodeAsBoolean(e).booleanValue();
378                break;
379              case ASN1Constants.UNIVERSAL_SET_TYPE:
380                final ASN1Set uuidSet = ASN1Set.decodeAsSet(e);
381                final ASN1Element[] uuidElements = uuidSet.elements();
382                entryUUIDs = new ArrayList<>(uuidElements.length);
383                for (final ASN1Element uuidElement : uuidElements)
384                {
385                  try
386                  {
387                    entryUUIDs.add(StaticUtils.decodeUUID(
388                         uuidElement.getValue()));
389                  }
390                  catch (final ParseException pe)
391                  {
392                    Debug.debugException(pe);
393                    throw new LDAPException(ResultCode.DECODING_ERROR,
394                         ERR_SYNC_INFO_IR_INVALID_UUID.get(type.name(),
395                              pe.getMessage()), pe);
396                  }
397                }
398                break;
399              default:
400                throw new LDAPException(ResultCode.DECODING_ERROR,
401                     ERR_SYNC_INFO_IR_VALUE_INVALID_SEQUENCE_TYPE.get(
402                          type.name(), StaticUtils.toHex(e.getType())));
403            }
404          }
405
406          if (entryUUIDs == null)
407          {
408            throw new LDAPException(ResultCode.DECODING_ERROR,
409                 ERR_SYNC_INFO_IR_NO_UUID_SET.get(type.name()));
410          }
411          break;
412      }
413    }
414    catch (final LDAPException le)
415    {
416      throw le;
417    }
418    catch (final Exception e)
419    {
420      Debug.debugException(e);
421
422      throw new LDAPException(ResultCode.DECODING_ERROR,
423           ERR_SYNC_INFO_IR_VALUE_DECODING_ERROR.get(
424                StaticUtils.getExceptionMessage(e)), e);
425    }
426
427    return new ContentSyncInfoIntermediateResponse(type, value, cookie,
428         refreshDone, refreshDeletes, entryUUIDs, r.getControls());
429  }
430
431
432
433  /**
434   * Encodes the provided information into a form suitable for use as the value
435   * of this intermediate response.
436   *
437   * @param  type            The type for this sync info message.
438   * @param  cookie          The updated sync state cookie.
439   * @param  refreshDone     Indicates whether the refresh phase of the
440   *                         synchronization operation is complete.
441   * @param  entryUUIDs      The set of entryUUIDs for the entries referenced
442   *                         in this message.
443   * @param  refreshDeletes  Indicates whether the associated entryUUIDs are for
444   *                         entries that have been removed.
445   *
446   * @return  The encoded value.
447   */
448  @NotNull()
449  private static ASN1OctetString encodeValue(
450                                      @NotNull final ContentSyncInfoType type,
451                                      @Nullable final ASN1OctetString cookie,
452                                      final boolean refreshDone,
453                                      @Nullable final List<UUID> entryUUIDs,
454                                      final boolean refreshDeletes)
455  {
456    final ASN1Element e;
457    switch (type)
458    {
459      case NEW_COOKIE:
460        e = new ASN1OctetString(type.getType(), cookie.getValue());
461        break;
462
463      case REFRESH_DELETE:
464      case REFRESH_PRESENT:
465        ArrayList<ASN1Element> l = new ArrayList<>(2);
466        if (cookie != null)
467        {
468          l.add(cookie);
469        }
470
471        if (! refreshDone)
472        {
473          l.add(new ASN1Boolean(refreshDone));
474        }
475
476        e = new ASN1Sequence(type.getType(), l);
477        break;
478
479      case SYNC_ID_SET:
480        l = new ArrayList<>(3);
481
482        if (cookie != null)
483        {
484          l.add(cookie);
485        }
486
487        if (refreshDeletes)
488        {
489          l.add(new ASN1Boolean(refreshDeletes));
490        }
491
492        final ArrayList<ASN1Element> uuidElements =
493             new ArrayList<>(entryUUIDs.size());
494        for (final UUID uuid : entryUUIDs)
495        {
496          uuidElements.add(new ASN1OctetString(StaticUtils.encodeUUID(uuid)));
497        }
498        l.add(new ASN1Set(uuidElements));
499
500        e = new ASN1Sequence(type.getType(), l);
501        break;
502
503      default:
504        // This should never happen.
505        throw new AssertionError("Unexpected sync info type:  " + type.name());
506    }
507
508    return new ASN1OctetString(e.encode());
509  }
510
511
512
513  /**
514   * Retrieves the type of content synchronization information represented in
515   * this response.
516   *
517   * @return  The type of content synchronization information represented in
518   *          this response.
519   */
520  @NotNull()
521  public ContentSyncInfoType getType()
522  {
523    return type;
524  }
525
526
527
528  /**
529   * Retrieves an updated state cookie for the synchronization session, if
530   * available.  It will always be non-{@code null} for a type of
531   * {@link ContentSyncInfoType#NEW_COOKIE}, and may or may not be {@code null}
532   * for other types.
533   *
534   * @return  An updated state cookie for the synchronization session, or
535   *          {@code null} if none is available.
536   */
537  @Nullable()
538  public ASN1OctetString getCookie()
539  {
540    return cookie;
541  }
542
543
544
545  /**
546   * Indicates whether the refresh phase of the synchronization operation has
547   * completed.  This is only applicable for the
548   * {@link ContentSyncInfoType#REFRESH_DELETE} and
549   * {@link ContentSyncInfoType#REFRESH_PRESENT} types.
550   *
551   * @return  {@code true} if the refresh phase of the synchronization operation
552   *          has completed, or {@code false} if not or if it is not applicable
553   *          for this message type.
554   */
555  public boolean refreshDone()
556  {
557    return refreshDone;
558  }
559
560
561
562  /**
563   * Retrieves a list of the entryUUID values for the entries referenced in this
564   * message.  This is only applicable for the
565   * {@link ContentSyncInfoType#SYNC_ID_SET} type.
566   *
567   * @return  A list of the entryUUID values for the entries referenced in this
568   *          message, or {@code null} if it is not applicable for this message
569   *          type.
570   */
571  @Nullable()
572  public List<UUID> getEntryUUIDs()
573  {
574    return entryUUIDs;
575  }
576
577
578
579  /**
580   * Indicates whether the provided set of UUIDs represent entries that have
581   * been removed.  This is only applicable for the
582   * {@link ContentSyncInfoType#SYNC_ID_SET} type.
583   *
584   * @return  {@code true} if the associated set of entryUUIDs represent entries
585   *          that have been deleted, or {@code false} if they represent entries
586   *          that remain unchanged or if it is not applicable for this message
587   *          type.
588   */
589  public boolean refreshDeletes()
590  {
591    return refreshDeletes;
592  }
593
594
595
596  /**
597   * {@inheritDoc}
598   */
599  @Override()
600  @NotNull()
601  public String getIntermediateResponseName()
602  {
603    return INFO_INTERMEDIATE_RESPONSE_NAME_SYNC_INFO.get();
604  }
605
606
607
608  /**
609   * {@inheritDoc}
610   */
611  @Override()
612  @NotNull()
613  public String valueToString()
614  {
615    final StringBuilder buffer = new StringBuilder();
616
617    buffer.append("syncInfoType='");
618    buffer.append(type.name());
619    buffer.append('\'');
620
621    if (cookie != null)
622    {
623      buffer.append(" cookie='");
624      StaticUtils.toHex(cookie.getValue(), buffer);
625      buffer.append('\'');
626    }
627
628    switch (type)
629    {
630      case REFRESH_DELETE:
631      case REFRESH_PRESENT:
632        buffer.append(" refreshDone='");
633        buffer.append(refreshDone);
634        buffer.append('\'');
635        break;
636
637      case SYNC_ID_SET:
638        buffer.append(" entryUUIDs={");
639
640        final Iterator<UUID> iterator = entryUUIDs.iterator();
641        while (iterator.hasNext())
642        {
643          buffer.append('\'');
644          buffer.append(iterator.next().toString());
645          buffer.append('\'');
646
647          if (iterator.hasNext())
648          {
649            buffer.append(',');
650          }
651        }
652
653        buffer.append('}');
654        break;
655
656      case NEW_COOKIE:
657      default:
658        // No additional content is needed.
659        break;
660    }
661
662    return buffer.toString();
663  }
664
665
666
667  /**
668   * {@inheritDoc}
669   */
670  @Override()
671  public void toString(@NotNull final StringBuilder buffer)
672  {
673    buffer.append("ContentSyncInfoIntermediateResponse(");
674
675    final int messageID = getMessageID();
676    if (messageID >= 0)
677    {
678      buffer.append("messageID=");
679      buffer.append(messageID);
680      buffer.append(", ");
681    }
682
683    buffer.append("type='");
684    buffer.append(type.name());
685    buffer.append('\'');
686
687    if (cookie != null)
688    {
689      buffer.append(", cookie='");
690      StaticUtils.toHex(cookie.getValue(), buffer);
691      buffer.append("', ");
692    }
693
694    switch (type)
695    {
696      case NEW_COOKIE:
697        // No additional content is needed.
698        break;
699
700      case REFRESH_DELETE:
701      case REFRESH_PRESENT:
702        buffer.append(", refreshDone=");
703        buffer.append(refreshDone);
704        break;
705
706      case SYNC_ID_SET:
707        buffer.append(", entryUUIDs={");
708
709        final Iterator<UUID> iterator = entryUUIDs.iterator();
710        while (iterator.hasNext())
711        {
712          buffer.append('\'');
713          buffer.append(iterator.next());
714          buffer.append('\'');
715          if (iterator.hasNext())
716          {
717            buffer.append(',');
718          }
719        }
720
721        buffer.append("}, refreshDeletes=");
722        buffer.append(refreshDeletes);
723        break;
724    }
725
726    buffer.append(')');
727  }
728}