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 *      Copyright 2014-2015 ForgeRock AS
024 */
025package org.opends.server.replication.protocol;
026
027import java.util.Collection;
028import java.util.zip.DataFormatException;
029
030import org.forgerock.opendj.io.ASN1;
031import org.forgerock.opendj.io.ASN1Reader;
032import org.forgerock.opendj.ldap.ByteSequenceReader;
033import org.forgerock.opendj.ldap.ByteString;
034import org.opends.server.replication.common.CSN;
035import org.opends.server.replication.common.ServerState;
036import org.opends.server.types.DN;
037import org.opends.server.types.DirectoryException;
038
039/**
040 * Byte array scanner class helps decode data from byte arrays received via
041 * messages over the replication protocol. Built on top of
042 * {@link ByteSequenceReader}, it isolates the latter against legacy type
043 * conversions from the replication protocol.
044 *
045 * @see ByteArrayBuilder ByteArrayBuilder class that encodes messages read with
046 *      current class.
047 */
048public class ByteArrayScanner
049{
050
051  private final ByteSequenceReader bytes;
052  private final byte[] byteArray;
053
054  /**
055   * Builds a ByteArrayScanner object that will read from the supplied byte
056   * array.
057   *
058   * @param bytes
059   *          the byte array input that will be read from
060   */
061  public ByteArrayScanner(byte[] bytes)
062  {
063    this.bytes = ByteString.wrap(bytes).asReader();
064    this.byteArray = bytes;
065  }
066
067  /**
068   * Reads the next boolean.
069   *
070   * @return the next boolean
071   * @throws DataFormatException
072   *           if no more data can be read from the input
073   */
074  public boolean nextBoolean() throws DataFormatException
075  {
076    return nextByte() != 0;
077  }
078
079  /**
080   * Reads the next byte.
081   *
082   * @return the next byte
083   * @throws DataFormatException
084   *           if no more data can be read from the input
085   */
086  public byte nextByte() throws DataFormatException
087  {
088    try
089    {
090      return bytes.readByte();
091    }
092    catch (IndexOutOfBoundsException e)
093    {
094      throw new DataFormatException(e.getMessage());
095    }
096  }
097
098  /**
099   * Reads the next short.
100   *
101   * @return the next short
102   * @throws DataFormatException
103   *           if no more data can be read from the input
104   */
105  public short nextShort() throws DataFormatException
106  {
107    try
108    {
109      return bytes.readShort();
110    }
111    catch (IndexOutOfBoundsException e)
112    {
113      throw new DataFormatException(e.getMessage());
114    }
115  }
116
117  /**
118   * Reads the next int.
119   *
120   * @return the next int
121   * @throws DataFormatException
122   *           if no more data can be read from the input
123   */
124  public int nextInt() throws DataFormatException
125  {
126    try
127    {
128      return bytes.readInt();
129    }
130    catch (IndexOutOfBoundsException e)
131    {
132      throw new DataFormatException(e.getMessage());
133    }
134  }
135
136  /**
137   * Reads the next long.
138   *
139   * @return the next long
140   * @throws DataFormatException
141   *           if no more data can be read from the input
142   */
143  public long nextLong() throws DataFormatException
144  {
145    try
146    {
147      return bytes.readLong();
148    }
149    catch (IndexOutOfBoundsException e)
150    {
151      throw new DataFormatException(e.getMessage());
152    }
153  }
154
155  /**
156   * Reads the next int that was encoded as a UTF8 string.
157   *
158   * @return the next int that was encoded as a UTF8 string.
159   * @throws DataFormatException
160   *           if no more data can be read from the input
161   */
162  public int nextIntUTF8() throws DataFormatException
163  {
164    return Integer.valueOf(nextString());
165  }
166
167  /**
168   * Reads the next long that was encoded as a UTF8 string.
169   *
170   * @return the next long that was encoded as a UTF8 string.
171   * @throws DataFormatException
172   *           if no more data can be read from the input
173   */
174  public long nextLongUTF8() throws DataFormatException
175  {
176    return Long.valueOf(nextString());
177  }
178
179  /**
180   * Reads the next UTF8-encoded string.
181   *
182   * @return the next UTF8-encoded string or null if the string length is zero
183   * @throws DataFormatException
184   *           if no more data can be read from the input
185   */
186  public String nextString() throws DataFormatException
187  {
188    try
189    {
190      final int offset = findZeroSeparator();
191      if (offset > 0)
192      {
193        final String s = bytes.readStringUtf8(offset);
194        skipZeroSeparator();
195        return s;
196      }
197      skipZeroSeparator();
198      return null;
199    }
200    catch (IndexOutOfBoundsException e)
201    {
202      throw new DataFormatException(e.getMessage());
203    }
204  }
205
206  private int findZeroSeparator() throws DataFormatException
207  {
208    int offset = 0;
209    final int remaining = bytes.remaining();
210    while (bytes.peek(offset) != 0 && offset < remaining)
211    {
212      offset++;
213    }
214    if (offset == remaining)
215    {
216      throw new DataFormatException("No more data to read from");
217    }
218    return offset;
219  }
220
221  /**
222   * Reads the next UTF8-encoded strings in the provided collection.
223   *
224   * @param output
225   *          the collection where to add the next UTF8-encoded strings
226   * @param <TCol>
227   *          the collection's concrete type
228   * @return the provided collection where the next UTF8-encoded strings have
229   *         been added.
230   * @throws DataFormatException
231   *           if no more data can be read from the input
232   */
233  public <TCol extends Collection<String>> TCol nextStrings(TCol output)
234      throws DataFormatException
235  {
236    // nextInt() would have been safer, but byte is compatible with legacy code.
237    final int colSize = nextByte();
238    for (int i = 0; i < colSize; i++)
239    {
240      output.add(nextString());
241    }
242    return output;
243  }
244
245  /**
246   * Reads the next CSN.
247   *
248   * @return the next CSN.
249   * @throws DataFormatException
250   *           if CSN was incorrectly encoded or no more data can be read from
251   *           the input
252   */
253  public CSN nextCSN() throws DataFormatException
254  {
255    try
256    {
257      return CSN.valueOf(bytes.readByteSequence(CSN.BYTE_ENCODING_LENGTH));
258    }
259    catch (IndexOutOfBoundsException e)
260    {
261      throw new DataFormatException(e.getMessage());
262    }
263  }
264
265  /**
266   * Reads the next CSN that was encoded as a UTF8 string.
267   *
268   * @return the next CSN that was encoded as a UTF8 string.
269   * @throws DataFormatException
270   *           if legacy CSN was incorrectly encoded or no more data can be read
271   *           from the input
272   */
273  public CSN nextCSNUTF8() throws DataFormatException
274  {
275    try
276    {
277      return CSN.valueOf(nextString());
278    }
279    catch (IndexOutOfBoundsException e)
280    {
281      throw new DataFormatException(e.getMessage());
282    }
283  }
284
285  /**
286   * Reads the next DN.
287   *
288   * @return the next DN.
289   * @throws DataFormatException
290   *           if DN was incorrectly encoded or no more data can be read from
291   *           the input
292   */
293  public DN nextDN() throws DataFormatException
294  {
295    try
296    {
297      return DN.valueOf(nextString());
298    }
299    catch (DirectoryException e)
300    {
301      throw new DataFormatException(e.getLocalizedMessage());
302    }
303  }
304
305  /**
306   * Return a new byte array containing all remaining bytes in this
307   * ByteArrayScanner.
308   *
309   * @return new byte array containing all remaining bytes
310   */
311  public byte[] remainingBytes()
312  {
313    final int length = byteArray.length - bytes.position();
314    return nextByteArray(length);
315  }
316
317  /**
318   * Return a new byte array containing all remaining bytes in this
319   * ByteArrayScanner bar the last one which is a zero terminated byte
320   * (compatible with legacy code).
321   *
322   * @return new byte array containing all remaining bytes bar the last one
323   */
324  public byte[] remainingBytesZeroTerminated()
325  {
326    /* do not copy stupid legacy zero separator */
327    final int length = byteArray.length - (bytes.position() + 1);
328    final byte[] result = nextByteArray(length);
329    bytes.skip(1); // ignore last (supposedly) zero byte
330    return result;
331  }
332
333  /**
334   * Return a new byte array containing the requested number of bytes.
335   *
336   * @param length
337   *          the number of bytes to be read and copied to the new byte array.
338   * @return new byte array containing the requested number of bytes.
339   */
340  public byte[] nextByteArray(final int length)
341  {
342    final byte[] result = new byte[length];
343    System.arraycopy(byteArray, bytes.position(), result, 0, length);
344    bytes.skip(length);
345    return result;
346  }
347
348  /**
349   * Reads the next ServerState.
350   * <p>
351   * Caution: ServerState MUST be the last field (see
352   * {@link ByteArrayBuilder#appendServerStateMustComeLast(ServerState)} javadoc).
353   * <p>
354   * Note: the super long method name it is intentional:
355   * nobody will want to use it, which is good because nobody should.
356   *
357   * @return the next ServerState.
358   * @throws DataFormatException
359   *           if ServerState was incorrectly encoded or no more data can be
360   *           read from the input
361   * @see ByteArrayBuilder#appendServerStateMustComeLast(ServerState)
362   */
363  public ServerState nextServerStateMustComeLast() throws DataFormatException
364  {
365    final ServerState result = new ServerState();
366
367    final int maxPos = byteArray.length - 1 /* stupid legacy zero separator */;
368    while (bytes.position() < maxPos)
369    {
370      final int serverId = nextIntUTF8();
371      final CSN csn = nextCSNUTF8();
372      if (serverId != csn.getServerId())
373      {
374        throw new DataFormatException("Expected serverId=" + serverId
375            + " to be the same as serverId for CSN=" + csn);
376      }
377      result.update(csn);
378    }
379    skipZeroSeparator();
380    return result;
381  }
382
383  /**
384   * Skips the next byte and verifies it is effectively the zero separator.
385   *
386   * @throws DataFormatException
387   *           if the next byte is not the zero separator.
388   */
389  public void skipZeroSeparator() throws DataFormatException
390  {
391    if (bytes.peek() != (byte) 0)
392    {
393      throw new DataFormatException("Expected a zero separator at position "
394          + bytes.position() + " but found byte " + bytes.peek());
395    }
396    bytes.skip(1);
397  }
398
399  /**
400   * Returns a new ASN1Reader that will read bytes from this ByteArrayScanner.
401   *
402   * @return a new ASN1Reader that will read bytes from this ByteArrayScanner.
403   */
404  public ASN1Reader getASN1Reader()
405  {
406    return ASN1.getReader(bytes);
407  }
408
409  /**
410   * Returns whether the scanner has more bytes to consume.
411   *
412   * @return true if the scanner has more bytes to consume, false otherwise.
413   */
414  public boolean isEmpty()
415  {
416    return bytes.remaining() == 0;
417  }
418
419  /** {@inheritDoc} */
420  @Override
421  public String toString()
422  {
423    return bytes.toString();
424  }
425}