/* * Copyright (c) 2012-2013 Spotify AB * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.spotify.netty4.handler.codec.zmtp; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.List; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.util.AbstractReferenceCounted; import io.netty.util.internal.RecyclableArrayList; import static com.spotify.netty4.handler.codec.zmtp.ZMTPUtils.checkNotNull; import static io.netty.buffer.ByteBufUtil.encodeString; import static io.netty.util.CharsetUtil.UTF_8; import static java.util.Arrays.asList; public class ZMTPMessage extends AbstractReferenceCounted implements Iterable<ByteBuf> { private final ByteBuf[] frames; private ZMTPMessage(final ByteBuf[] frames) { this.frames = checkNotNull(frames, "frames"); } @Override public ZMTPMessage retain() { super.retain(); return this; } @Override public ZMTPMessage retain(final int increment) { super.retain(increment); return this; } /** * Create a new message from a string frames, using UTF-8 encoding. */ public static ZMTPMessage fromUTF8(final CharSequence... strings) { return from(ByteBufAllocator.DEFAULT, UTF_8, strings); } /** * Create a new message from a string frames, using UTF-8 encoding. */ public static ZMTPMessage fromUTF8(final ByteBufAllocator alloc, final CharSequence... strings) { return from(alloc, UTF_8, strings); } /** * Create a new message from a list of string frames, using UTF-8 encoding. */ public static ZMTPMessage fromUTF8(final Iterable<? extends CharSequence> strings) { return from(UTF_8, strings); } /** * Create a new message from a list of string frames, using UTF-8 encoding. */ public static ZMTPMessage fromUTF8(final ByteBufAllocator alloc, final Iterable<? extends CharSequence> strings) { return from(alloc, UTF_8, strings); } /** * Create a new message from a list of string frames, using a specified encoding. */ public static ZMTPMessage from(final Charset charset, final CharSequence... strings) { return from(charset, asList(strings)); } /** * Create a new message from a list of string frames, using a specified encoding. */ public static ZMTPMessage from(final ByteBufAllocator alloc, final Charset charset, final CharSequence... strings) { return from(alloc, charset, asList(strings)); } /** * Create a new message from a list of string frames, using a specified encoding. */ public static ZMTPMessage from(final Charset charset, final Iterable<? extends CharSequence> strings) { return from(ByteBufAllocator.DEFAULT, charset, strings); } /** * Create a new message from a list of string frames, using a specified encoding. */ public static ZMTPMessage from(final ByteBufAllocator alloc, final Charset charset, final Iterable<? extends CharSequence> strings) { final List<ByteBuf> frames = new ArrayList<ByteBuf>(); for (final CharSequence string : strings) { frames.add(encodeString(alloc, CharBuffer.wrap(string), charset)); } return from(frames); } /** * Create a new message from a list of frames. */ public static ZMTPMessage from(final Collection<ByteBuf> frames) { checkNotNull(frames, "frames"); return new ZMTPMessage(frames.toArray(new ByteBuf[frames.size()])); } /** * Create a new message from a list of frames. */ public static ZMTPMessage from(final ByteBuf[] frames) { return new ZMTPMessage(frames.clone()); } public int size() { return frames.length; } @Override public Iterator<ByteBuf> iterator() { return new FrameIterator(); } /** * Get a specific frame. */ public ByteBuf frame(final int i) { return frames[i]; } @Override protected void deallocate() { for (final ByteBuf frame : frames) { frame.release(); } } @Override public boolean equals(final Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } final ZMTPMessage byteBufs = (ZMTPMessage) o; return Arrays.equals(frames, byteBufs.frames); } @Override public int hashCode() { return frames != null ? Arrays.hashCode(frames) : 0; } @Override public String toString() { return "ZMTPMessage{" + toString(frames) + '}'; } /** * Create a human readable string representation of binary data, keeping printable ascii and hex * encoding everything else. * * @param data The data * @return A human readable string representation of the data. */ private static String toString(final ByteBuf data) { if (data == null) { return null; } final StringBuilder sb = new StringBuilder(); for (int i = data.readerIndex(); i < data.writerIndex(); i++) { final byte b = data.getByte(i); if (b > 31 && b < 127) { if (b == '%') { sb.append('%'); } sb.append((char) b); } else { sb.append('%'); sb.append(String.format("%02X", b)); } } return sb.toString(); } /** * Create a human readable string representation of a list of ZMTP frames, keeping printable ascii * and hex encoding everything else. * * @param frames The ZMTP frames. * @return A human readable string representation of the frames. */ private static String toString(final ByteBuf[] frames) { final StringBuilder builder = new StringBuilder("["); for (int i = 0; i < frames.length; i++) { final ByteBuf frame = frames[i]; builder.append('"'); builder.append(toString(frame)); builder.append('"'); if (i < frames.length - 1) { builder.append(','); } } builder.append(']'); return builder.toString(); } /** * Convenience method for reading a {@link ZMTPMessage} from a {@link ByteBuf}. */ public static ZMTPMessage read(final ByteBuf in, final ZMTPVersion version) throws ZMTPParsingException { final int mark = in.readerIndex(); final ZMTPWireFormat wireFormat = ZMTPWireFormats.wireFormat(version); final ZMTPWireFormat.Header header = wireFormat.header(); final RecyclableArrayList frames = RecyclableArrayList.newInstance(); while (true) { final boolean read = header.read(in); if (!read) { frames.recycle(); in.readerIndex(mark); return null; } if (in.readableBytes() < header.length()) { frames.recycle(); in.readerIndex(mark); return null; } if (header.length() > Integer.MAX_VALUE) { throw new ZMTPParsingException("frame is too large: " + header.length()); } final ByteBuf frame = in.readSlice((int) header.length()); frame.retain(); frames.add(frame); if (!header.more()) { @SuppressWarnings("unchecked") final ZMTPMessage message = ZMTPMessage.from((List<ByteBuf>) (Object) frames); frames.recycle(); return message; } } } /** * Convenience method for writing a {@link ZMTPMessage} to a {@link ByteBuf}. */ public ByteBuf write(final ZMTPVersion version) { return write(ByteBufAllocator.DEFAULT, version); } /** * Convenience method for writing a {@link ZMTPMessage} to a {@link ByteBuf}. */ public ByteBuf write(final ByteBufAllocator alloc, final ZMTPVersion version) { final ZMTPMessageEncoder encoder = new ZMTPMessageEncoder(); final ZMTPEstimator estimator = ZMTPEstimator.create(version); encoder.estimate(this, estimator); final ByteBuf out = alloc.buffer(estimator.size()); final ZMTPWriter writer = ZMTPWriter.create(version); writer.reset(out); encoder.encode(this, writer); return out; } /** * Convenience method for writing a {@link ZMTPMessage} to a {@link ByteBuf}. */ public void write(final ByteBuf out, final ZMTPVersion version) { final ZMTPWriter writer = ZMTPWriter.create(version); final ZMTPMessageEncoder encoder = new ZMTPMessageEncoder(); writer.reset(out); encoder.encode(this, writer); } /** * Create a new {@link ZMTPMessage} with a frame added at the front. */ public ZMTPMessage push(final ByteBuf frame) { for (final ByteBuf f : frames) { f.retain(); } final ByteBuf[] frames = new ByteBuf[this.frames.length + 1]; frames[0] = frame; System.arraycopy(this.frames, 0, frames, 1, this.frames.length); return new ZMTPMessage(frames); } /** * Create a new {@link ZMTPMessage} with the front frame removed. */ public ZMTPMessage pop() { if (this.frames.length == 0) { throw new IllegalStateException("empty message"); } final ByteBuf[] frames = new ByteBuf[this.frames.length - 1]; System.arraycopy(this.frames, 1, frames, 0, frames.length); for (final ByteBuf f : frames) { f.retain(); } return new ZMTPMessage(frames); } /** * Iterates over the frames of the {@link ZMTPMessage}. */ private class FrameIterator implements Iterator<ByteBuf> { int i; @Override public boolean hasNext() { return i < frames.length; } @Override public ByteBuf next() { return frames[i++]; } @Override public void remove() { throw new UnsupportedOperationException("remove"); } } }