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