/* * Copyright (C) 2019 Toshiaki Maki <[email protected]> * * 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 am.ik.rsocket; import java.io.IOException; import java.io.PrintStream; import java.io.UncheckedIOException; import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.Unpooled; import io.rsocket.metadata.CompositeMetadataFlyweight; import io.rsocket.metadata.WellKnownMimeType; import io.rsocket.transport.ClientTransport; import joptsimple.OptionParser; import joptsimple.OptionSet; import joptsimple.OptionSpec; import reactor.netty.tcp.TcpClient; import reactor.util.function.Tuple2; import reactor.util.function.Tuples; import static am.ik.rsocket.Transport.TCP; import static am.ik.rsocket.Transport.WEBSOCKET; import static java.util.stream.Collectors.toList; public class Args { private final OptionParser parser = new OptionParser(); private final OptionSpec<Void> version = parser.acceptsAll(Arrays.asList("v", "version"), "Print version"); private final OptionSpec<Void> help = parser.acceptsAll(Arrays.asList("help"), "Print help"); private final OptionSpec<Void> wiretap = parser.acceptsAll(Arrays.asList("w", "wiretap"), "Enable wiretap"); private final OptionSpec<Void> debug = parser.acceptsAll(Arrays.asList("debug"), "Enable FrameLogger"); private final OptionSpec<Void> quiet = parser.acceptsAll(Arrays.asList("q", "quiet"), "Disable the output on next"); private final OptionSpec<InteractionModel> interactionModel = parser .acceptsAll(Arrays.asList("im", "interactionModel"), "InteractionModel").withOptionalArg() .ofType(InteractionModel.class).defaultsTo(InteractionModel.REQUEST_RESPONSE); private final OptionSpec<Void> stream = parser.acceptsAll(Arrays.asList("stream"), "Shortcut of --im REQUEST_STREAM"); private final OptionSpec<Void> request = parser.acceptsAll(Arrays.asList("request"), "Shortcut of --im REQUEST_RESPONSE"); private final OptionSpec<Void> fnf = parser.acceptsAll(Arrays.asList("fnf"), "Shortcut of --im FIRE_AND_FORGET"); private final OptionSpec<Void> channel = parser.acceptsAll(Arrays.asList("channel"), "Shortcut of --im REQUEST_CHANNEL"); private final OptionSpec<Integer> resume = parser.acceptsAll(Arrays.asList("resume"), "Enable resume. Resume session duration can be configured in seconds. Unless the duration is specified, the default value (2min) is used.") .withOptionalArg().ofType(Integer.class); private final URI uri; private final OptionSpec<String> dataMimeType = parser .acceptsAll(Arrays.asList("dataMimeType", "dmt"), "MimeType for data").withOptionalArg() .defaultsTo(WellKnownMimeType.APPLICATION_JSON.getString()); private final OptionSpec<String> metadataMimeType = parser .acceptsAll(Arrays.asList("metadataMimeType", "mmt"), "MimeType for metadata (default: text/plain)") .withOptionalArg(); private final OptionSpec<String> data = parser .acceptsAll(Arrays.asList("d", "data"), "Data. Use '-' to read data from standard input.").withOptionalArg() .defaultsTo(""); private final OptionSpec<String> metadata = parser .acceptsAll(Arrays.asList("m", "metadata"), "Metadata (default: )").withOptionalArg(); private final OptionSpec<String> setup = parser.acceptsAll(Arrays.asList("s", "setup"), "Setup payload") .withOptionalArg(); private final OptionSpec<String> route = parser .acceptsAll(Arrays.asList("route", "r"), "Routing Metadata Extension").withOptionalArg(); private final OptionSpec<String> log = parser.acceptsAll(Arrays.asList("log"), "Enable log()").withOptionalArg(); private final OptionSpec<Integer> limitRate = parser .acceptsAll(Arrays.asList("limitRate"), "Enable limitRate(rate)").withOptionalArg().ofType(Integer.class); private final OptionSpec<Integer> take = parser.acceptsAll(Arrays.asList("take"), "Enable take(n)") .withOptionalArg().ofType(Integer.class); private final OptionSpec<Long> delayElements = parser .acceptsAll(Arrays.asList("delayElements"), "Enable delayElements(delay) in milli seconds") .withOptionalArg().ofType(Long.class); private final OptionSpec<Void> stacktrace = parser.acceptsAll(Arrays.asList("stacktrace"), "Show Stacktrace when an exception happens"); private final OptionSpec<Void> showSystemProperties = parser.acceptsAll(Arrays.asList("show-system-properties"), "Show SystemProperties for troubleshoot"); private final OptionSpec<String> wsHeader = parser.acceptsAll(Arrays.asList("wsh", "wsHeader"), "Header for web socket connection") .withOptionalArg(); private final OptionSet options; private Tuple2<String, ByteBuf> composedMetadata = null; public Args(String[] args) { final OptionSpec<String> uri = parser.nonOptions().describedAs("Uri"); this.options = parser.parse(args); this.uri = Optional.ofNullable(uri.value(this.options)).map(URI::create).orElse(null); } public boolean hasUri() { return this.uri != null; } public String host() { return this.uri.getHost(); } public int port() { final int port = this.uri.getPort(); if (port < 0) { if (secure()) { return 443; } else { return 80; } } return port; } public boolean secure() { final String scheme = this.uri.getScheme(); return scheme.endsWith("+tls") || scheme.equals("wss"); } public String path() { return this.uri.getPath(); } public InteractionModel interactionModel() { if (this.options.has(this.stream)) { return InteractionModel.REQUEST_STREAM; } if (this.options.has(this.request)) { return InteractionModel.REQUEST_RESPONSE; } if (this.options.has(this.channel)) { return InteractionModel.REQUEST_CHANNEL; } if (this.options.has(this.fnf)) { return InteractionModel.FIRE_AND_FORGET; } return this.options.valueOf(this.interactionModel); } public ByteBuf data() { return Unpooled.wrappedBuffer(this.options.valueOf(this.data).getBytes(StandardCharsets.UTF_8)); } public boolean readFromStdin() { return "-".equals(this.options.valueOf(this.data)); } public String route() { return this.options.valueOf(this.route); } public String dataMimeType() { final String mimeType = this.options.valueOf(this.dataMimeType); try { return WellKnownMimeType.valueOf(mimeType).getString(); } catch (IllegalArgumentException ignored) { return mimeType; } } public Optional<ByteBuf> setup() { if (this.options.has(this.setup) && this.options.valueOf(this.setup) != null) { return Optional .of(Unpooled.wrappedBuffer(this.options.valueOf(this.setup).getBytes(StandardCharsets.UTF_8))); } else { return Optional.empty(); } } /** * https://github.com/rsocket/rsocket/blob/master/Extensions/CompositeMetadata.md */ public Tuple2<String, ByteBuf> composeMetadata() { if (this.composedMetadata != null) { return this.composedMetadata; } final List<String> mimeTypeList = this.metadataMimeType(); final List<ByteBuf> metadataList = this.metadata(); if (metadataList.size() != mimeTypeList.size()) { throw new IllegalArgumentException( String.format("The size of metadata(%d) and metadataMimeType(%d) don't match!", metadataList.size(), mimeTypeList.size())); } if (metadataList.isEmpty()) { return Tuples.of(WellKnownMimeType.TEXT_PLAIN.getString(), Unpooled.buffer()); } if (metadataList.size() == 1) { return Tuples.of(mimeTypeList.get(0), metadataList.get(0)); } final CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer(); final ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; final Iterator<String> mimeTypeIterator = mimeTypeList.iterator(); final Iterator<ByteBuf> metadataIterator = metadataList.iterator(); while (mimeTypeIterator.hasNext()) { final String mimeType = mimeTypeIterator.next(); final ByteBuf metadata = metadataIterator.next(); final WellKnownMimeType wellKnownMimeType = WellKnownMimeType.fromString(mimeType); if (wellKnownMimeType != WellKnownMimeType.UNPARSEABLE_MIME_TYPE) { CompositeMetadataFlyweight.encodeAndAddMetadata(compositeByteBuf, allocator, wellKnownMimeType, metadata); } else { CompositeMetadataFlyweight.encodeAndAddMetadata(compositeByteBuf, allocator, mimeType, metadata); } } this.composedMetadata = Tuples.of(WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString(), compositeByteBuf); return this.composedMetadata; } List<ByteBuf> metadata() { List<ByteBuf> list = new ArrayList<>(); if (this.options.has(this.route)) { list.add(routingMetadata(this.route())); } list.addAll(this.options.valuesOf(this.metadata).stream() .map(metadata -> Unpooled.wrappedBuffer(metadata.getBytes(StandardCharsets.UTF_8))).collect(toList())); return list; } List<String> metadataMimeType() { List<String> list = new ArrayList<>(); if (this.options.has(this.route)) { list.add(WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString()); } list.addAll(this.options.valuesOf(this.metadataMimeType).stream().map(mimeType -> { try { return WellKnownMimeType.valueOf(mimeType).getString(); } catch (IllegalArgumentException ignored) { return mimeType; } }).collect(toList())); return list; } Map<String, String> wsHeaders() { Map<String, Set<String>> headerSet = new LinkedHashMap<>(); this.options.valuesOf(wsHeader).forEach(header -> { String[] nameValue = header.split(":", 2); if (nameValue.length == 2) { headerSet.computeIfAbsent(nameValue[0], k -> new LinkedHashSet<>()).add(nameValue[1]); } }); Map<String, String> headers = new LinkedHashMap<>(); headerSet.forEach((key, value) -> headers.put(key, String.join(";", value))); return headers; } public ClientTransport clientTransport() { final String scheme = this.uri.getScheme(); Transport transport; if (scheme.startsWith("ws")) { transport = WEBSOCKET; } else if (scheme.startsWith("tcp")) { transport = TCP; } else { throw new IllegalArgumentException(scheme + " is unsupported scheme."); } return transport.clientTransport(this); } public TcpClient tcpClient() { final TcpClient tcpClient = TcpClient.create().host(this.host()).port(this.port()).wiretap(this.wiretap()); if (this.secure()) { return tcpClient.secure(); } return tcpClient; } public boolean wiretap() { return this.options.has(this.wiretap); } public boolean debug() { return this.options.has(this.debug); } public boolean quiet() { return this.options.has(this.quiet); } public boolean stacktrace() { return this.options.has(this.stacktrace); } public Optional<Duration> resume() { if (this.options.has(this.resume)) { final Integer duration = this.options.valueOf(this.resume); return Optional.of(duration == null ? Duration.ofMinutes(2) : Duration.ofSeconds(duration)); } else { return Optional.empty(); } } public Optional<Integer> limitRate() { if (this.options.has(this.limitRate)) { return Optional.ofNullable(this.options.valueOf(this.limitRate)); } else { return Optional.empty(); } } public Optional<Integer> take() { if (this.options.has(this.take)) { return Optional.ofNullable(this.options.valueOf(this.take)); } else { return Optional.empty(); } } public Optional<Duration> delayElements() { if (this.options.has(this.delayElements)) { return Optional.of(Duration.ofMillis(this.options.valueOf(this.delayElements))); } else { return Optional.empty(); } } public Optional<String> log() { if (this.options.has(this.log)) { return Optional.of(Objects.toString(this.options.valueOf(this.log), "rsc")); } else { return Optional.empty(); } } public boolean help() { return this.options.has(this.help); } public boolean version() { return this.options.has(this.version); } public void printHelp(PrintStream stream) { try { stream.println("usage: rsc Uri [Options]"); stream.println(); this.parser.printHelpOn(stream); } catch (IOException e) { throw new UncheckedIOException(e); } } public boolean showSystemProperties() { return this.options.has(this.showSystemProperties); } /** * https://github.com/rsocket/rsocket/blob/master/Extensions/Routing.md */ static ByteBuf routingMetadata(String tag) { final byte[] bytes = tag.getBytes(StandardCharsets.UTF_8); final ByteBuf buf = Unpooled.buffer(); buf.writeByte(bytes.length); buf.writeBytes(bytes); return buf; } }