parts) throws MalformedException
95 | {
96 | Parameter[] params = new Parameter[parts.size()];
97 | String part = null; int i = 0;
98 | while(!parts.isEmpty() && (part = parts.remove(0)) != null) {
99 | params[i++] = doParameter(part);
100 | }
101 | return params;
102 | }
103 |
104 | }
105 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/mime/impl/ComplexValueHeader.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: 3/18/11 11:55 PM
5 | */
6 | package xpertss.mime.impl;
7 |
8 | import xpertss.lang.Strings;
9 | import xpertss.mime.Header;
10 | import xpertss.mime.HeaderValue;
11 | import xpertss.lang.Objects;
12 |
13 | public class ComplexValueHeader implements Header {
14 |
15 | private final String name;
16 | private final Type type;
17 | private final HeaderValue[] values;
18 |
19 | public ComplexValueHeader(String name, Type type, HeaderValue[] values)
20 | {
21 | this.name = Objects.notNull(name, "name");
22 | this.type = Objects.notNull(type, "type");
23 | this.values = Objects.notNull(values, "values");
24 | }
25 |
26 | public String getName()
27 | {
28 | return name;
29 | }
30 |
31 | public String getValue()
32 | {
33 | StringBuilder buf = new StringBuilder();
34 | for(HeaderValue value : values) {
35 | if(buf.length() > 0) buf.append(", ");
36 | buf.append(value.toString());
37 | }
38 | return buf.toString();
39 | }
40 |
41 | public Type getType()
42 | {
43 | return type;
44 | }
45 |
46 | public int size()
47 | {
48 | return values.length;
49 | }
50 |
51 | public HeaderValue getValue(int index)
52 | {
53 | return values[index];
54 | }
55 |
56 | public HeaderValue getValue(String name)
57 | {
58 | for(HeaderValue value : values)
59 | if(Strings.equalIgnoreCase(name, value.getName())) return value;
60 | return null;
61 | }
62 |
63 | public String toString()
64 | {
65 | return String.format("%s: %s", getName(), getValue());
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/mime/impl/HttpHeaderParserProvider.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: 3/18/11 1:51 PM
5 | */
6 | package xpertss.mime.impl;
7 |
8 | import xpertss.mime.Header;
9 | import xpertss.mime.HeaderParser;
10 | import xpertss.mime.spi.HeaderParserProvider;
11 |
12 | import java.util.HashMap;
13 | import java.util.Map;
14 |
15 | /**
16 | * This implementation breaks down impl headers into two main categories plus a few
17 | * header by header implementations. The two main categories include: SimpleHeaders
18 | * which have only a single unnamed value and ComplexHeaders which have multiple named
19 | * and unnamed values.
20 | *
21 | * In addition to these two main categories which cover most of the possible impl
22 | * headers there are also a few header specific parsers for Cookies as an example
23 | * which do not conform to the standard parameter format.
24 | *
25 | * The main reason for the two main categories is that the special character ,
26 | * has two different meanings depending on the header. In some cases it denotes a
27 | * boundary between values such as is the case on the Allow header. In other cases it
28 | * denotes a separation of elements within a value such as its use in all Date related
29 | * headers. Due to this difference two header implementations needed to be developed to
30 | * parse that usage differently.
31 | */
32 | public class HttpHeaderParserProvider implements HeaderParserProvider {
33 |
34 | private Map parsers = new HashMap<>();
35 |
36 | public HttpHeaderParserProvider()
37 | {
38 | // General Headers (Cache-Control, Connection, Date, Pragma, Trailer, Transfer-Encoding, Upgrade, Via, Warning)
39 | parsers.put("CACHE-CONTROL", new ComplexHeaderParser("Cache-Control", Header.Type.General));
40 | parsers.put("CONNECTION", new SimpleHeaderParser("Connection", Header.Type.General));
41 | parsers.put("DATE", new SimpleHeaderParser("Date", Header.Type.General));
42 | parsers.put("PRAGMA", new SimpleHeaderParser("Pragma", Header.Type.General));
43 | parsers.put("TRAILER", new ComplexHeaderParser("Trailer", Header.Type.General));
44 | parsers.put("TRANSFER-ENCODING", new ComplexHeaderParser("Transfer-Encoding", Header.Type.General));
45 | parsers.put("UPGRADE", new ComplexHeaderParser("Upgrade", Header.Type.General));
46 | parsers.put("VIA", new ComplexHeaderParser("Via", Header.Type.General));
47 | // TODO Warning
48 |
49 |
50 | // Request Headers (Accept, Accept-Charset, Accept-Encoding, Accept-Language, Authorization, Expect, From, Host,
51 | // If-Match, If-Modified-Since, If-None-Match, If-Range, If-Unmodified-Since, Max-Forwards,
52 | // Proxy-Authorization, Range, Referer, TE, User-Agent)
53 | parsers.put("ACCEPT", new ComplexHeaderParser("Accept", Header.Type.Request));
54 | parsers.put("ACCEPT-CHARSET", new ComplexHeaderParser("Accept-Charset", Header.Type.Request));
55 | parsers.put("ACCEPT-ENCODING", new ComplexHeaderParser("Accept-Encoding", Header.Type.Request));
56 | parsers.put("ACCEPT-LANGUAGE", new ComplexHeaderParser("Accept-Language", Header.Type.Request));
57 | parsers.put("EXPECT", new ComplexHeaderParser("Expect", Header.Type.Request));
58 | parsers.put("FROM", new SimpleHeaderParser("From", Header.Type.Request));
59 | parsers.put("HOST", new SimpleHeaderParser("Host", Header.Type.Request));
60 | parsers.put("IF-MATCH", new ComplexHeaderParser("If-Match", Header.Type.Request));
61 | parsers.put("IF-MODIFIED-SINCE", new SimpleHeaderParser("If-Modified-Since", Header.Type.Request));
62 | parsers.put("IF-NONE-MATCH", new ComplexHeaderParser("If-None-Match", Header.Type.Request));
63 | parsers.put("IF-RANGE", new SimpleHeaderParser("If-Range", Header.Type.Request));
64 | parsers.put("IF-UNMODIFIED-SINCE", new SimpleHeaderParser("If-Unmodified-Since", Header.Type.Request));
65 | parsers.put("MAX-FORWARDS", new SimpleHeaderParser("Max-Forwards", Header.Type.Request));
66 | parsers.put("RANGE", new ComplexHeaderParser("Range", Header.Type.Request));
67 | parsers.put("REFERER", new SimpleHeaderParser("Referer", Header.Type.Request));
68 | parsers.put("TE", new ComplexHeaderParser("TE", Header.Type.Request));
69 | parsers.put("USER-AGENT", new SimpleHeaderParser("User-Agent", Header.Type.Request));
70 |
71 | parsers.put("AUTHORIZATION", new SimpleHeaderParser("Authorization", Header.Type.Request)); // kind of unique
72 | parsers.put("PROXY-AUTHORIZATION", new SimpleHeaderParser("Proxy-Authorization", Header.Type.Request)); // kind of unique
73 | // TODO Cookie
74 |
75 |
76 | // Response Headers (Accept-Ranges, Age, ETag, Location, Proxy-Authenticate, Retry-After, Server, Vary,
77 | // WWW-Authenticate)
78 | parsers.put("ACCEPT-RANGES", new ComplexHeaderParser("Accept-Ranges", Header.Type.Response));
79 | parsers.put("AGE", new SimpleHeaderParser("Age", Header.Type.Response));
80 | parsers.put("ETAG", new SimpleHeaderParser("ETag", Header.Type.Response));
81 | parsers.put("LOCATION", new SimpleHeaderParser("Location", Header.Type.Response));
82 | parsers.put("RETRY-AFTER", new SimpleHeaderParser("Retry-After", Header.Type.Response));
83 | parsers.put("SERVER", new SimpleHeaderParser("Server", Header.Type.Response));
84 | parsers.put("VARY", new ComplexHeaderParser("Vary", Header.Type.Response));
85 |
86 | parsers.put("PROXY-AUTHENTICATE", new SimpleHeaderParser("Proxy-Authenticate", Header.Type.Response)); // kind of unique
87 | parsers.put("WWW-AUTHENTICATE", new SimpleHeaderParser("WWW-Authenticate", Header.Type.Response)); // kind of unique
88 | // TODO Set-Cookie and possibly Set-Cookie2
89 |
90 |
91 | // Entity Headers (Allow, Content-Encoding, Content-Length, Content-Location, Content-MD5, Content-Range,
92 | // Content-Type, Expires, Last-Modified)
93 | parsers.put("ALLOW", new ComplexHeaderParser("Allow", Header.Type.Entity));
94 | parsers.put("CONTENT-ENCODING", new ComplexHeaderParser("Content-Encoding", Header.Type.Entity));
95 | parsers.put("CONTENT-LANGUAGE", new ComplexHeaderParser("Content-Language", Header.Type.Entity));
96 | parsers.put("CONTENT-LENGTH", new SimpleHeaderParser("Content-Length", Header.Type.Entity));
97 | parsers.put("CONTENT-LOCATION", new SimpleHeaderParser("Content-Location", Header.Type.Entity));
98 | parsers.put("CONTENT-MD5", new SimpleHeaderParser("Content-MD5", Header.Type.Entity));
99 | parsers.put("CONTENT-RANGE", new SimpleHeaderParser("Content-Range", Header.Type.Entity));
100 | parsers.put("CONTENT-TYPE", new SimpleHeaderParser("Content-Type", Header.Type.Entity));
101 | parsers.put("EXPIRES", new SimpleHeaderParser("Expires", Header.Type.Entity));
102 | parsers.put("LAST-MODIFIED", new SimpleHeaderParser("Last-Modified", Header.Type.Entity));
103 |
104 | }
105 |
106 | public HeaderParser create(String name)
107 | {
108 | return parsers.get(name.toUpperCase());
109 | }
110 |
111 |
112 |
113 | }
114 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/mime/impl/MailHeaderParserProvider.java:
--------------------------------------------------------------------------------
1 | package xpertss.mime.impl;
2 |
3 | import xpertss.mime.Header;
4 | import xpertss.mime.HeaderParser;
5 | import xpertss.mime.spi.HeaderParserProvider;
6 |
7 | import java.util.HashMap;
8 | import java.util.Map;
9 |
10 | /**
11 | *
12 | */
13 | public class MailHeaderParserProvider implements HeaderParserProvider {
14 |
15 | private Map parsers = new HashMap<>();
16 |
17 | public MailHeaderParserProvider()
18 | {
19 | // rfc822
20 | parsers.put("RESENT-MESSAGE-ID", new SimpleHeaderParser("Resent-Message-ID", Header.Type.General));
21 | parsers.put("IN-REPLY-TO", new ComplexHeaderParser("In-Reply-To", Header.Type.General));
22 | parsers.put("MESSAGE-ID", new SimpleHeaderParser("Message-ID", Header.Type.General));
23 | parsers.put("REFERENCES", new ComplexHeaderParser("References", Header.Type.General));
24 |
25 | parsers.put("RETURN-PATH", new SimpleHeaderParser("Return-Path", Header.Type.General));
26 | parsers.put("RECEIVED", new SimpleHeaderParser("Received", Header.Type.General));
27 | parsers.put("RESENT-DATE", new SimpleHeaderParser("Resent-Date", Header.Type.General));
28 |
29 | parsers.put("RESENT-SENDER", new SimpleHeaderParser("Resent-Sender", Header.Type.General));
30 | parsers.put("RESENT-FROM", new SimpleHeaderParser("Resent-From", Header.Type.General));
31 | parsers.put("REPLY-TO", new SimpleHeaderParser("Reply-To", Header.Type.General));
32 | parsers.put("SENDER", new SimpleHeaderParser("Sender", Header.Type.General));
33 |
34 |
35 | parsers.put("TO", new ComplexHeaderParser("To", Header.Type.General));
36 | parsers.put("CC", new ComplexHeaderParser("Cc", Header.Type.General));
37 |
38 | parsers.put("SUBJECT", new SimpleHeaderParser("Subject", Header.Type.General));
39 | parsers.put("COMMENTS", new SimpleHeaderParser("Comments", Header.Type.General));
40 | parsers.put("KEYWORDS", new ComplexHeaderParser("Keywords", Header.Type.General));
41 |
42 |
43 |
44 | // Resent-Sender
45 | // Resent-To
46 | // Resent-Reply-To
47 |
48 |
49 |
50 |
51 | // rfc2045
52 | parsers.put("MIME-VERSION", new SimpleHeaderParser("MIME-Version", Header.Type.General));
53 | parsers.put("CONTENT-ID", new SimpleHeaderParser("Content-ID", Header.Type.General));
54 | parsers.put("CONTENT-TRANSFER-ENCODING", new SimpleHeaderParser("Content-Transfer-Encoding", Header.Type.General));
55 | parsers.put("CONTENT-DESCRIPTION", new SimpleHeaderParser("Content-Description", Header.Type.General));
56 | parsers.put("CONTENT-DISPOSITION", new SimpleHeaderParser("Content-Disposition", Header.Type.General));
57 | parsers.put("CONTENT-DURATION", new SimpleHeaderParser("Content-Duration", Header.Type.General));
58 |
59 |
60 |
61 |
62 | // TODO Add Mail Headers
63 | // https://tools.ietf.org/html/rfc2045
64 | // https://tools.ietf.org/html/rfc2046
65 | // https://tools.ietf.org/html/rfc2047
66 | // https://tools.ietf.org/html/rfc1123
67 | // https://tools.ietf.org/html/rfc822
68 | }
69 |
70 | @Override
71 | public HeaderParser create(String name)
72 | {
73 | return parsers.get(name.toUpperCase());
74 | }
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/mime/impl/NamedHeaderValue.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: 3/18/11 9:45 PM
5 | */
6 | package xpertss.mime.impl;
7 |
8 | import xpertss.lang.Strings;
9 | import xpertss.mime.HeaderValue;
10 | import xpertss.mime.Parameter;
11 | import xpertss.lang.Objects;
12 |
13 | public class NamedHeaderValue implements HeaderValue {
14 |
15 | private final String name;
16 | private final String value;
17 | private final Parameter[] params;
18 |
19 | public NamedHeaderValue(String name, String value, Parameter[] params)
20 | {
21 | this.name = Objects.notNull(name, "name");
22 | this.value = Objects.notNull(value, "value");
23 | this.params = Objects.notNull(params, "params");
24 | }
25 |
26 | public String getName()
27 | {
28 | return name;
29 | }
30 |
31 | public String getValue()
32 | {
33 | if(value.indexOf('"') == 0 && value.lastIndexOf('"') == value.length() - 1) {
34 | return value.substring(1, value.length() - 1);
35 | }
36 | return value;
37 | }
38 |
39 | public int size()
40 | {
41 | return params.length;
42 | }
43 |
44 | public Parameter getParameter(int index)
45 | {
46 | return params[index];
47 | }
48 |
49 | public Parameter getParameter(String name)
50 | {
51 | for(Parameter param : params) {
52 | if(Strings.equalIgnoreCase(name, param.getName())) return param;
53 | }
54 | return null;
55 | }
56 |
57 | public String toString()
58 | {
59 | StringBuilder buf = new StringBuilder(getName()).append("=").append(value);
60 | for(Parameter param : params) {
61 | buf.append("; ").append(param.toString());
62 | }
63 | return buf.toString();
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/mime/impl/ParameterizedHeaderParser.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: 3/19/11 8:34 AM
5 | */
6 | package xpertss.mime.impl;
7 |
8 | import xpertss.mime.HeaderParser;
9 | import xpertss.mime.HeaderTokenizer;
10 | import xpertss.mime.MalformedException;
11 | import xpertss.mime.Parameter;
12 | import xpertss.utils.Utils;
13 |
14 | public abstract class ParameterizedHeaderParser extends HeaderParser {
15 |
16 | protected Parameter doParameter(String rawParam) throws MalformedException
17 | {
18 | HeaderTokenizer h = new HeaderTokenizer(rawParam, HeaderTokenizer.MIME);
19 | StringBuilder buf = new StringBuilder();
20 | String name = null, value = null;
21 | boolean quoted = false;
22 | while(true) {
23 | HeaderTokenizer.Token token = h.next();
24 | switch(token.getType()) {
25 | case HeaderTokenizer.Token.EOF:
26 | value = Utils.trimAndClear(buf);
27 | return (quoted) ? new QuotedParameter(name, value) : new SimpleParameter(name, value);
28 | case HeaderTokenizer.Token.LWS:
29 | buf.append(" "); // collapse whitespace
30 | continue;
31 | case '=':
32 | if(name != null) throw new MalformedException("malformed parameter");
33 | name = Utils.trimAndClear(buf);
34 | continue;
35 | case HeaderTokenizer.Token.QUOTEDSTRING:
36 | quoted = true;
37 | default:
38 | buf.append(token.getValue());
39 | }
40 | }
41 | }
42 |
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/mime/impl/QuotedParameter.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: 3/19/11 12:31 AM
5 | */
6 | package xpertss.mime.impl;
7 |
8 | import xpertss.mime.Parameter;
9 | import xpertss.lang.Objects;
10 |
11 | // TODO Must a quoted parameter be a name/value pair or could it be simply a value??
12 | public class QuotedParameter implements Parameter {
13 |
14 | private final String name;
15 | private final String value;
16 |
17 | public QuotedParameter(String name, String value)
18 | {
19 | this.name = Objects.notNull(name, "name");
20 | this.value = Objects.notNull(value, "value");
21 | }
22 |
23 |
24 | public String getName()
25 | {
26 | return name;
27 | }
28 |
29 | public String getValue()
30 | {
31 | return value;
32 | }
33 |
34 | public String toString()
35 | {
36 | return name + "=\"" + value + "\"";
37 | }
38 |
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/mime/impl/RtspHeaderParserProvider.java:
--------------------------------------------------------------------------------
1 | package xpertss.mime.impl;
2 |
3 | import xpertss.mime.Header;
4 | import xpertss.mime.HeaderParser;
5 | import xpertss.mime.spi.HeaderParserProvider;
6 |
7 | import java.util.HashMap;
8 | import java.util.Map;
9 |
10 | /**
11 | * A collection of headers found in the Rtsp specification.
12 | */
13 | public class RtspHeaderParserProvider implements HeaderParserProvider {
14 |
15 | private Map parsers = new HashMap();
16 |
17 | // RTSP Headers not already defined in HTTP
18 | public RtspHeaderParserProvider()
19 | {
20 | parsers.put("BANDWIDTH", new SimpleHeaderParser("Bandwidth", Header.Type.Request));
21 | parsers.put("BLOCKSIZE", new SimpleHeaderParser("Blocksize", Header.Type.Request));
22 | parsers.put("CONFERENCE", new SimpleHeaderParser("Conference", Header.Type.Request));
23 | parsers.put("CONTENT-BASE", new SimpleHeaderParser("Content-Base", Header.Type.Entity));
24 | parsers.put("CSEQ", new SimpleHeaderParser("CSeq", Header.Type.General));
25 | parsers.put("PROXY-REQUIRE", new SimpleHeaderParser("Proxy-Require", Header.Type.Request));
26 | parsers.put("PUBLIC", new ComplexHeaderParser("Public", Header.Type.Response));
27 | parsers.put("REQUIRE", new SimpleHeaderParser("Require", Header.Type.Request));
28 | parsers.put("RTP-INFO", new ComplexHeaderParser("RTP-Info", Header.Type.Response));
29 | parsers.put("SCALE", new SimpleHeaderParser("Scale", Header.Type.General));
30 | parsers.put("SPEED", new SimpleHeaderParser("Speed", Header.Type.General));
31 | parsers.put("SESSION", new SimpleHeaderParser("Session", Header.Type.General));
32 | parsers.put("TIMESTAMP", new SimpleHeaderParser("Timestamp", Header.Type.General));
33 | parsers.put("TRANSPORT", new ComplexHeaderParser("Transport", Header.Type.General));
34 | parsers.put("UNSUPPORTED", new SimpleHeaderParser("Unsupported", Header.Type.Response));
35 | }
36 |
37 | public HeaderParser create(String name)
38 | {
39 | return parsers.get(name.toUpperCase());
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/mime/impl/SimpleHeaderParser.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: 3/18/11 1:52 PM
5 | */
6 | package xpertss.mime.impl;
7 |
8 | import xpertss.mime.Header;
9 | import xpertss.mime.HeaderTokenizer;
10 | import xpertss.mime.HeaderValue;
11 | import xpertss.mime.MalformedException;
12 | import xpertss.mime.Parameter;
13 | import xpertss.lang.Objects;
14 | import xpertss.utils.Utils;
15 |
16 | import java.util.ArrayList;
17 | import java.util.List;
18 |
19 | import static xpertss.mime.HeaderTokenizer.MIME;
20 | import static xpertss.mime.HeaderTokenizer.Token;
21 |
22 | /**
23 | * A simple header parser parses simple impl headers. A simple header is one that has only
24 | * a single unnamed value. Some examples include, Date, Server, Content-Length, Expires,
25 | * Age, Content-Type, Location, Referrer, etc.
26 | */
27 | public class SimpleHeaderParser extends ParameterizedHeaderParser {
28 |
29 | private final Header.Type type;
30 | private final String name;
31 |
32 | protected SimpleHeaderParser(String name, Header.Type type)
33 | {
34 | this.name = Objects.notNull(name, "name");
35 | this.type = Objects.notNull(type, "type");
36 | }
37 |
38 |
39 | @Override
40 | protected Header doParse(CharSequence raw)
41 | throws MalformedException
42 | {
43 | HeaderTokenizer h = new HeaderTokenizer(raw, MIME);
44 | List parts = new ArrayList();
45 | StringBuilder buf = new StringBuilder();
46 | boolean complete = false;
47 |
48 | while(!complete) {
49 | HeaderTokenizer.Token token = h.next();
50 | switch(token.getType()) {
51 | case Token.EOF:
52 | parts.add(Utils.trimAndClear(buf));
53 | complete = true;
54 | break;
55 | case Token.LWS:
56 | buf.append(" "); // collapse whitespace
57 | continue;
58 | case ';':
59 | parts.add(Utils.trimAndClear(buf));
60 | continue;
61 | case Token.QUOTEDSTRING:
62 | buf.append('"').append(token.getValue()).append('"');
63 | continue;
64 | case Token.COMMENT:
65 | buf.append('(').append(token.getValue()).append(')');
66 | continue;
67 | default:
68 | buf.append(token.getValue());
69 | }
70 | }
71 | return new SingleValueHeader(name, type, create(parts));
72 | }
73 |
74 |
75 | private HeaderValue create(List parts) throws MalformedException
76 | {
77 | if(parts.size() == 1) return new SimpleHeaderValue(parts.get(0), new Parameter[0]);
78 | Parameter[] params = new Parameter[parts.size() - 1];
79 | for(int i = 1; i < parts.size(); i++) params[i-1] = doParameter(parts.get(i));
80 | return new SimpleHeaderValue(parts.get(0), params);
81 |
82 | }
83 |
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/mime/impl/SimpleHeaderValue.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: 3/18/11 9:30 PM
5 | */
6 | package xpertss.mime.impl;
7 |
8 | import xpertss.lang.Strings;
9 | import xpertss.mime.HeaderValue;
10 | import xpertss.mime.Parameter;
11 | import xpertss.lang.Objects;
12 |
13 | public class SimpleHeaderValue implements HeaderValue {
14 |
15 | private final Parameter[] params;
16 | private final String value;
17 |
18 | public SimpleHeaderValue(String value, Parameter[] params)
19 | {
20 | this.value = Objects.notNull(value, "value");
21 | this.params = Objects.notNull(params, "params");
22 | }
23 |
24 |
25 | public String getName()
26 | {
27 | return null;
28 | }
29 |
30 | public String getValue()
31 | {
32 | if(value.indexOf('"') == 0 && value.lastIndexOf('"') == value.length() - 1) {
33 | return value.substring(1, value.length() - 1);
34 | }
35 | return value;
36 | }
37 |
38 | public int size()
39 | {
40 | return params.length;
41 | }
42 |
43 | public Parameter getParameter(int index)
44 | {
45 | return params[index];
46 | }
47 |
48 | public Parameter getParameter(String name)
49 | {
50 | for(Parameter param : params) {
51 | if(Strings.equalIgnoreCase(name, param.getName())) return param;
52 | }
53 | return null;
54 | }
55 |
56 | public String toString()
57 | {
58 | StringBuilder buf = new StringBuilder(value);
59 | for(Parameter param : params) {
60 | buf.append("; ").append(param.toString());
61 | }
62 | return buf.toString();
63 | }
64 |
65 |
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/mime/impl/SimpleParameter.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: 3/18/11 9:42 PM
5 | */
6 | package xpertss.mime.impl;
7 |
8 | import xpertss.lang.Objects;
9 | import xpertss.mime.Parameter;
10 |
11 | public class SimpleParameter implements Parameter {
12 |
13 | private final String name;
14 | private final String value;
15 |
16 | public SimpleParameter(String name, String value)
17 | {
18 | this.name = name;
19 | this.value = Objects.notNull(value, "value");
20 | }
21 |
22 |
23 | public String getName()
24 | {
25 | return name;
26 | }
27 |
28 | public String getValue()
29 | {
30 | return value;
31 | }
32 |
33 | public String toString()
34 | {
35 | return (name == null) ? value : name + "=" + value;
36 | }
37 |
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/mime/impl/SingleValueHeader.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: 3/18/11 11:53 PM
5 | */
6 | package xpertss.mime.impl;
7 |
8 | import xpertss.mime.Header;
9 | import xpertss.mime.HeaderValue;
10 | import xpertss.lang.Objects;
11 |
12 | public class SingleValueHeader implements Header {
13 |
14 | private final Type type;
15 | private final String name;
16 | private final HeaderValue[] value;
17 |
18 | public SingleValueHeader(String name, Type type, HeaderValue value)
19 | {
20 | this.name = Objects.notNull(name, "name");
21 | this.type = Objects.notNull(type, "type");
22 | this.value = new HeaderValue[] { value };
23 | }
24 |
25 | public String getName()
26 | {
27 | return name;
28 | }
29 |
30 | public String getValue()
31 | {
32 | return value[0].toString();
33 | }
34 |
35 | public Type getType()
36 | {
37 | return type;
38 | }
39 |
40 | public int size()
41 | {
42 | return 1;
43 | }
44 |
45 | public HeaderValue getValue(int index)
46 | {
47 | return value[index];
48 | }
49 |
50 | public HeaderValue getValue(String name)
51 | {
52 | return null;
53 | }
54 |
55 | public String toString()
56 | {
57 | return String.format("%s: %s", getName(), getValue());
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/mime/spi/HeaderParserProvider.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: 3/18/11 11:16 AM
5 | */
6 | package xpertss.mime.spi;
7 |
8 | import xpertss.mime.HeaderParser;
9 |
10 | /**
11 | * Service provider interface defining a means to locate HeaderParser's capable
12 | * of parsing named header values.
13 | */
14 | public interface HeaderParserProvider {
15 |
16 | /**
17 | * Return a HeaderParser implementation for the named header or {@code null}
18 | * if this provider does not support the given header.
19 | *
20 | * @param name The header name for which a parser is desired
21 | * @return A parser capable of parsing the named header or {@code null}
22 | */
23 | public HeaderParser create(String name);
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/net/OptionalSocketOptions.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: 4/3/2015
5 | */
6 | package xpertss.net;
7 |
8 | import java.net.SocketOption;
9 |
10 | public final class OptionalSocketOptions {
11 |
12 | /**
13 | * A socket option which gives an implementation hints as to how it would
14 | * like it to timeout read operations. This is heavily implementation
15 | * dependent. For example a protocol that uses requests and responses may
16 | * use this to timeout a request if it is not received quickly enough.
17 | */
18 | public static final SocketOption SO_TIMEOUT =
19 | new StdSocketOption("SO_TIMEOUT", Integer.class);
20 |
21 | /**
22 | * A socket option which dictates the maximum number of pending incoming
23 | * connections to queue in the backlog before rejecting them outright.
24 | * This would in most cases only apply to an Acceptor.
25 | */
26 | public static final SocketOption TCP_BACKLOG =
27 | new StdSocketOption("TCP_BACKLOG", Integer.class);
28 |
29 |
30 | private static class StdSocketOption implements SocketOption {
31 | private final String name;
32 | private final Class type;
33 | StdSocketOption(String name, Class type) {
34 | this.name = name;
35 | this.type = type;
36 | }
37 | @Override public String name() { return name; }
38 | @Override public Class type() { return type; }
39 | @Override public String toString() { return name; }
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/net/SSLSocketOptions.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: 4/3/2015
5 | */
6 | package xpertss.net;
7 |
8 | import java.net.SocketOption;
9 |
10 | /**
11 | * Defines a set socket options useful for sessions that support SSL.
12 | *
13 | * The {@link SocketOption#name name} of each socket option defined by this class
14 | * is its field name.
15 | */
16 | public final class SSLSocketOptions {
17 |
18 | private SSLSocketOptions() { }
19 |
20 |
21 |
22 | /**
23 | * Enable or disable client authentication requests.
24 | *
25 | * The value of this socket option is an SslClientAuth that dictates whether a server
26 | * socket is required to request client authentication during the handshake.
27 | *
28 | * This is only valid on SSL enabled acceptors.
29 | */
30 | public static final SocketOption SSL_CLIENT_AUTH =
31 | new TlsSocketOption("SSL_CLIENT_AUTH", SslClientAuth.class);
32 |
33 | /**
34 | * Set the cipher suites SSL sockets will support during negotiation.
35 | *
36 | * The value of this socket option is a String[] that specifies the SSL cipher suites
37 | * the socket will accept during handshaking. This list must be supported by the
38 | * SSLContext that was configured for use. Only cipher suites in this list will be
39 | * negotiated during the handshake. Peers that do not support any of the cipher
40 | * suites in this list will not be able to establish a connection.
41 | *
42 | * This is only valid on SSL enabled connectors and acceptors.
43 | */
44 | public static final SocketOption SSL_CIPHER_SUITES =
45 | new TlsSocketOption("SSL_CIPHER_SUITES", String[].class);
46 |
47 | /**
48 | * Set the protocols SSL sockets will support during negotiation.
49 | *
50 | * The value of this socket option is a String[] that specifies the protocols the
51 | * socket will accept. This list must be supported by the SSLContext that was
52 | * configured for use. Protocols in this case means SSLv3, TLSv1, SSLv2 as examples.
53 | * Peers that do not support any of the protocols in this list will not be able to
54 | * establish a connection.
55 | *
56 | * This is only valid on SSL enabled connectors and acceptors.
57 | */
58 | public static final SocketOption SSL_PROTOCOLS =
59 | new TlsSocketOption("SSL_PROTOCOLS", String[].class);
60 |
61 |
62 |
63 |
64 | private static class TlsSocketOption implements java.net.SocketOption {
65 | private final String name;
66 | private final Class type;
67 | TlsSocketOption(String name, Class type) {
68 | this.name = name;
69 | this.type = type;
70 | }
71 | @Override public String name() { return name; }
72 | @Override public Class type() { return type; }
73 | @Override public String toString() { return name; }
74 | }
75 |
76 |
77 | }
78 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/net/SocketOptions.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: 4/3/2015
5 | */
6 | package xpertss.net;
7 |
8 | import xpertss.nio.NioSession;
9 |
10 | import java.net.SocketOption;
11 |
12 | public class SocketOptions {
13 |
14 |
15 | public static boolean set(NioSession session, SocketOption option, T value)
16 | {
17 | try { session.setOption(option, value); } catch(Exception e) { return false; }
18 | return true;
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/net/SslClientAuth.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: 2/26/11 11:25 AM
5 | */
6 | package xpertss.net;
7 |
8 | /**
9 | * An enumeration of SSL Client Authentication options.
10 | */
11 | public enum SslClientAuth {
12 |
13 | /**
14 | * Request client authentication but proceed if it is not provided.
15 | */
16 | Want,
17 |
18 | /**
19 | * Request client authentication and terminate negotiation if it is not
20 | * provided.
21 | */
22 | Need,
23 |
24 | /**
25 | * Do not request client authentication.
26 | */
27 | None
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/nio/AcceptHandler.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: Aug 12, 2007
5 | * Time: 11:28:38 PM
6 | */
7 |
8 | package xpertss.nio;
9 |
10 | import java.io.IOException;
11 |
12 | public interface AcceptHandler extends Selectable {
13 |
14 | public void handleAccept() throws IOException;
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/nio/Checkable.java:
--------------------------------------------------------------------------------
1 | package xpertss.nio;
2 |
3 | import java.io.IOException;
4 |
5 | /**
6 | * A {@link Selectable} subclass which wishes to be periodically checked so
7 | * that it may validate timeouts and other state.
8 | */
9 | public interface Checkable extends Selectable {
10 |
11 | public void handleCheckup() throws IOException;
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/nio/ConnectHandler.java:
--------------------------------------------------------------------------------
1 | package xpertss.nio;
2 |
3 | import java.io.IOException;
4 |
5 | /**
6 | */
7 | public interface ConnectHandler extends Selectable {
8 |
9 | public void handleConnect() throws IOException;
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/nio/DataHandler.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: Aug 12, 2007
5 | * Time: 11:33:54 PM
6 | */
7 |
8 | package xpertss.nio;
9 |
10 | import java.io.IOException;
11 |
12 | public interface DataHandler extends Selectable {
13 |
14 | public void handleRead() throws IOException;
15 | public void handleWrite() throws IOException;
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/nio/DeferredNioAction.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: 3/26/11 2:19 PM
5 | */
6 | package xpertss.nio;
7 |
8 | /**
9 | * An extension to NioAction who's execution will be deferred until the next
10 | * loop of the reactor thread. By default execute calls into the reactor will
11 | * execute immediately if the calling thread is the reactor thread. This can
12 | * sometimes be undesirable as you want some other action to occur first. By
13 | * implementing the DeferredNioAction interface the action will be enqueued
14 | * for the next loop allowing all other operations to proceed in the meantime.
15 | */
16 | public interface DeferredNioAction extends NioAction {
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/nio/NioAction.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: Aug 12, 2007
5 | * Time: 11:19:27 PM
6 | */
7 |
8 | package xpertss.nio;
9 |
10 | import java.io.IOException;
11 | import java.nio.channels.Selector;
12 |
13 | public interface NioAction {
14 |
15 | public void execute(Selector selector)
16 | throws IOException;
17 |
18 | public Selectable getSelectable();
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/nio/NioProvider.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: 3/14/11 12:59 PM
5 | */
6 | package xpertss.nio;
7 |
8 | public interface NioProvider {
9 |
10 | public Thread newThread(Runnable r);
11 |
12 | public void serviceException(Exception error);
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/nio/NioReactor.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: 3/14/11 12:53 PM
5 | */
6 | package xpertss.nio;
7 |
8 | import xpertss.io.NIOUtils;
9 | import xpertss.lang.Objects;
10 | import xpertss.threads.Threads;
11 |
12 | import java.nio.channels.SelectionKey;
13 | import java.nio.channels.Selector;
14 | import java.util.ArrayList;
15 | import java.util.Collections;
16 | import java.util.List;
17 | import java.util.Set;
18 | import java.util.concurrent.TimeUnit;
19 |
20 | public class NioReactor implements Runnable, NioService {
21 |
22 | private final List actions = Collections.synchronizedList(new ArrayList());
23 | private volatile Thread thread;
24 | private NioProvider provider;
25 | private Selector selector;
26 |
27 | public NioReactor(NioProvider provider)
28 | {
29 | this.provider = Objects.notNull(provider, "provider");
30 | }
31 |
32 |
33 | public void execute(NioAction action)
34 | {
35 | synchronized(this) {
36 | if(thread != Thread.currentThread() || action instanceof DeferredNioAction) {
37 | if(thread == null) activate();
38 | actions.add(action);
39 | selector.wakeup();
40 | } else {
41 | executeNow(action);
42 | }
43 | }
44 | }
45 |
46 | public boolean isSelectorThread()
47 | {
48 | return Thread.currentThread() == thread;
49 | }
50 |
51 |
52 |
53 | /**
54 | * Returns true if this server is active, false otherwise.
55 | */
56 | public boolean isActive()
57 | {
58 | return (thread != null);
59 | }
60 |
61 |
62 |
63 |
64 | /**
65 | * Waits for this server to shutdown.
66 | */
67 | public void await()
68 | {
69 | Threads.join(thread);
70 | }
71 |
72 | /**
73 | * Wait for the specified amount of time for this server to shutdown. This
74 | * will return false if this returned because it timed out before the server
75 | * completely shutdown, otherwise it will return true.
76 | *
77 | * @param timeout the time to wait
78 | * @param unit the unit the timeout value is measured with
79 | * @return True if the server shutdown within the allotted time
80 | */
81 | public boolean await(long timeout, TimeUnit unit)
82 | {
83 | Threads.join(thread, timeout, unit);
84 | return (thread == null);
85 | }
86 |
87 |
88 |
89 |
90 |
91 | private void executeNow(NioAction action)
92 | {
93 | try {
94 | action.execute(selector);
95 | } catch(Exception ex) {
96 | action.getSelectable().shutdown(ex);
97 | }
98 | }
99 |
100 |
101 | private void passivate()
102 | {
103 | synchronized(this) {
104 | thread = null;
105 | NIOUtils.close(selector);
106 | selector = null;
107 | }
108 | }
109 |
110 | private void activate()
111 | {
112 | selector = NIOUtils.openSelector();
113 | thread = provider.newThread(this);
114 | thread.start();
115 | }
116 |
117 |
118 | public void run()
119 | {
120 | do {
121 | try {
122 |
123 | int count = selector.select(10);
124 |
125 | // Now execute any pending NIO Actions
126 | while(actions.size() > 0) executeNow(actions.remove(0));
127 |
128 | // Process NIO events
129 | if(count > 0) {
130 | Set selectedKeys = selector.selectedKeys();
131 | for(SelectionKey sk : selectedKeys) {
132 | if(sk.attachment() instanceof Selectable) {
133 | Selectable select = (Selectable) sk.attachment();
134 | try {
135 | if(select instanceof AcceptHandler) {
136 | AcceptHandler handler = (AcceptHandler) select;
137 | if(sk.isValid() && sk.isAcceptable()) handler.handleAccept();
138 | } else if(select instanceof DataHandler) {
139 | DataHandler handler = (DataHandler) select;
140 | if(sk.isValid() && sk.isReadable()) handler.handleRead();
141 | if(sk.isValid() && sk.isWritable()) handler.handleWrite();
142 | if(select instanceof ConnectHandler) {
143 | ConnectHandler connectable = (ConnectHandler) select;
144 | if(sk.isValid() && sk.isConnectable()) connectable.handleConnect();
145 | }
146 | } else {
147 | assert false : select.getClass().getName();
148 | }
149 | } catch(Exception ex) {
150 | select.shutdown(ex);
151 | }
152 | } else {
153 | assert sk.attachment() != null : "selection key with no attachment";
154 | assert false : sk.attachment().getClass().getName();
155 | }
156 | }
157 | selectedKeys.clear();
158 | }
159 |
160 |
161 | // Now check the status of all user sockets
162 | Set keys = selector.keys();
163 | for(SelectionKey key : keys) {
164 | if(key.isValid() && key.attachment() instanceof Checkable) {
165 | Checkable session = (Checkable) key.attachment();
166 | try { session.handleCheckup(); } catch(Exception ex) { session.shutdown(ex); }
167 | }
168 | }
169 |
170 | } catch(Exception e) {
171 | provider.serviceException(e);
172 | }
173 | } while(!selector.keys().isEmpty() || !actions.isEmpty());
174 | passivate();
175 | }
176 |
177 |
178 | }
179 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/nio/NioReader.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: 3/23/11 9:23 AM
5 | */
6 | package xpertss.nio;
7 |
8 | import java.io.IOException;
9 | import java.nio.ByteBuffer;
10 |
11 | public interface NioReader {
12 |
13 | public boolean readFrom(ByteBuffer src) throws IOException;
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/nio/NioService.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: 3/14/11 11:24 AM
5 | */
6 | package xpertss.nio;
7 |
8 | public interface NioService {
9 |
10 | public void execute(NioAction action);
11 |
12 | public boolean isSelectorThread();
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/nio/NioSession.java:
--------------------------------------------------------------------------------
1 | package xpertss.nio;
2 |
3 | import java.io.IOException;
4 | import java.net.SocketAddress;
5 | import java.net.SocketOption;
6 | import java.util.Set;
7 |
8 | /**
9 | */
10 | public interface NioSession {
11 |
12 |
13 |
14 |
15 |
16 | /**
17 | * Get the current ready status of this session.
18 | */
19 | public ReadyState getReadyState();
20 |
21 |
22 |
23 |
24 | /**
25 | * Returns the socket address of the remote peer.
26 | *
27 | * @return The socket address of the remote peer.
28 | */
29 | public SocketAddress getRemoteAddress();
30 |
31 | /**
32 | * Returns the socket address of local machine which is associated with this
33 | * session.
34 | *
35 | * @return The local socket address for this session.
36 | */
37 | public SocketAddress getLocalAddress();
38 |
39 | /**
40 | * This will return the listen address that established this IOSession if it
41 | * is a server side session. Otherwise, if it is a client side session this
42 | * will return the same value as {@link #getRemoteAddress()}.
43 | *
44 | * @return The service address associated with this session.
45 | */
46 | public SocketAddress getServiceAddress();
47 |
48 |
49 |
50 |
51 |
52 |
53 | /**
54 | * Sets the value of a socket option.
55 | *
56 | * @param name The socket option
57 | * @param value The value of the socket option. A value of {@code null}
58 | * may be a valid value for some socket options.
59 | * @return This session
60 | *
61 | * @throws UnsupportedOperationException If the socket option is not
62 | * supported by this session
63 | * @throws IllegalArgumentException If the value is not a valid value
64 | * for this socket option
65 | * @throws IOException If an I/O error occurs
66 | * @see java.net.StandardSocketOptions
67 | * @see xpertss.net.OptionalSocketOptions
68 | * @see xpertss.net.SSLSocketOptions
69 | */
70 | NioSession setOption(SocketOption name, T value) throws IOException;
71 |
72 | /**
73 | * Returns the value of a socket option.
74 | *
75 | * @param name The socket option
76 | * @return The value of the socket option. A value of {@code null} may
77 | * be a valid value for some socket options.
78 | * @throws UnsupportedOperationException If the socket option is not
79 | * supported by this channel
80 | * @throws IOException If an I/O error occurs
81 | * @see java.net.StandardSocketOptions
82 | * @see xpertss.net.OptionalSocketOptions
83 | * @see xpertss.net.SSLSocketOptions
84 | */
85 | T getOption(SocketOption name) throws IOException;
86 |
87 | /**
88 | * Returns a set of the socket options supported by this channel.
89 | *
90 | * This method will continue to return the set of options even after
91 | * the session has been closed.
92 | *
93 | * @return A set of the socket options supported by this session
94 | */
95 | Set> supportedOptions();
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | /**
106 | * Returns the time in millis when this session was created. The time is
107 | * always measured as the number of milliseconds since midnight, January
108 | * 1, 1970 UTC.
109 | *
110 | * @return The time in millis when this session was created.
111 | */
112 | public long getCreationTime();
113 |
114 | /**
115 | * Returns the time in millis when the last I/O operation occurred. The
116 | * time is always measured as the number of milliseconds since midnight,
117 | * January 1, 1970 UTC.
118 | *
119 | * @return The time in millis when the last I/O operation occured.
120 | */
121 | public long getLastIoTime();
122 |
123 | /**
124 | * Returns the time in millis when the last read operation occurred. The
125 | * time is always measured as the number of milliseconds since midnight,
126 | * January 1, 1970 UTC.
127 | *
128 | * @return The time in millis when the last read operation occurred
129 | */
130 | public long getLastReadTime();
131 |
132 | /**
133 | * Returns the time in millis when the lst write operation occurred. The
134 | * time is always measured as the number of milliseconds since midnight,
135 | * January 1, 1970 UTC.
136 | *
137 | * @return The time in millis when the lst write operation occurred
138 | */
139 | public long getLastWriteTime();
140 |
141 |
142 |
143 |
144 |
145 | /**
146 | * Return the number of bytes written to this session.
147 | */
148 | public long getBytesWritten();
149 |
150 | /**
151 | * Return the number of bytes read from this session.
152 | */
153 | public long getBytesRead();
154 |
155 |
156 |
157 |
158 |
159 | /**
160 | * Returns the value of user-defined attribute of this session.
161 | *
162 | * @param key the key of the attribute
163 | * @return null if there is no attribute with the specified key
164 | */
165 | public Object getAttribute(String key);
166 |
167 | /**
168 | * Sets a user-defined attribute.
169 | *
170 | * @param key the key of the attribute
171 | * @param value the value of the attribute
172 | */
173 | public void setAttribute(String key, Object value);
174 |
175 | /**
176 | * Removes a user-defined attribute with the specified key.
177 | *
178 | * @param key The key identifying the attribute to remove
179 | */
180 | public void removeAttribute(String key);
181 |
182 | /**
183 | * Returns true if this session contains the attribute with
184 | * the specified key.
185 | *
186 | * @param key The key identifying the attribute to check existance of
187 | * @return true if the named attribute exists in this session
188 | */
189 | public boolean hasAttribute(String key);
190 |
191 | /**
192 | * Returns the set of keys of all user-defined attributes.
193 | *
194 | * @return A set of attribute keys currently defined
195 | */
196 | public Set getAttributeKeys();
197 |
198 |
199 |
200 |
201 | }
202 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/nio/NioStats.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: 3/23/11 1:39 PM
5 | */
6 | package xpertss.nio;
7 |
8 | /**
9 | */
10 | public class NioStats {
11 |
12 | private long time = System.currentTimeMillis();
13 | private long count;
14 |
15 | public void record(long amount)
16 | {
17 | if(amount > 0) {
18 | time = System.currentTimeMillis();
19 | count += amount;
20 | }
21 | }
22 |
23 | public long getCount()
24 | {
25 | return count;
26 | }
27 |
28 | public long getTime()
29 | {
30 | return time;
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/nio/NioWriter.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: 3/23/11 9:24 AM
5 | */
6 | package xpertss.nio;
7 |
8 | import java.io.IOException;
9 | import java.nio.ByteBuffer;
10 |
11 | public interface NioWriter {
12 |
13 | public boolean writeTo(ByteBuffer dst) throws IOException;
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/nio/ReadyState.java:
--------------------------------------------------------------------------------
1 | package xpertss.nio;
2 |
3 | /**
4 | *
5 | */
6 | public enum ReadyState {
7 |
8 | Open,
9 |
10 | Connecting,
11 |
12 | Connected,
13 |
14 | Closing,
15 |
16 | Closed
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/nio/Selectable.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: Aug 12, 2007
5 | * Time: 11:17:40 PM
6 | */
7 |
8 | package xpertss.nio;
9 |
10 | import java.nio.channels.SelectableChannel;
11 |
12 | public interface Selectable {
13 |
14 | public SelectableChannel getChannel();
15 |
16 | public void shutdown(Exception ex);
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/rtsp/RtspClient.java:
--------------------------------------------------------------------------------
1 | package xpertss.rtsp;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 | import xpertss.lang.Objects;
6 | import xpertss.nio.NioProvider;
7 | import xpertss.nio.NioReactor;
8 | import xpertss.threads.Threads;
9 | import xpertss.util.Version;
10 | import xpertss.utils.UserAgent;
11 |
12 | import java.net.URI;
13 | import java.util.concurrent.ThreadFactory;
14 | import java.util.concurrent.TimeUnit;
15 |
16 | /**
17 | */
18 | public class RtspClient {
19 |
20 |
21 | private Logger log = LoggerFactory.getLogger(getClass());
22 |
23 | // Product name OS/version Java/version
24 | private final NioReactor reactor;
25 | private ThreadFactory factory;
26 | private UserAgent userAgent;
27 |
28 |
29 | public RtspClient()
30 | {
31 | this.reactor = new NioReactor(new RtspNioProvider());
32 | }
33 |
34 |
35 |
36 |
37 |
38 |
39 | /**
40 | * Call this to have a configured (and attached to the reactor service)
41 | * session. It will not be connected initially.
42 | *
43 | * @throws NullPointerException if uri is {@code null}
44 | * @throws IllegalArgumentException if the uri is invalid
45 | */
46 | public RtspSession open(RtspHandler handler, URI uri)
47 | {
48 | if(!Objects.notNull(uri).getScheme().equals("rtsp"))
49 | throw new IllegalArgumentException("only supports rtsp urls");
50 | return new RtspSession(reactor, handler, uri, getUserAgent());
51 | }
52 |
53 |
54 |
55 |
56 |
57 |
58 | /**
59 | * Set the UserAgent string this client will submit to servers with each
60 | * request.
61 | */
62 | public void setUserAgent(UserAgent userAgent)
63 | {
64 | this.userAgent = Objects.notNull(userAgent, "userAgent");
65 | }
66 |
67 | /**
68 | * Get the UserAgent string this client is submitting to servers with each
69 | * request.
70 | */
71 | public UserAgent getUserAgent()
72 | {
73 | return (userAgent == null) ? UserAgent.create("Aries", new Version(1,0)) : userAgent;
74 | }
75 |
76 |
77 |
78 | /**
79 | * Set the thread factory this HttpServer will use to obtain threads from.
80 | *
81 | * It is advised that the thread factory should create daemon threads with
82 | * a priority slightly above NORM_PRIORITY.
83 | *
84 | * @param factory The thread factory this server should obtain threads from
85 | * @throws IllegalStateException if this is called on an active server
86 | */
87 | public void setThreadFactory(ThreadFactory factory)
88 | {
89 | if(isActive()) throw new IllegalStateException("server is active");
90 | this.factory = factory;
91 | }
92 |
93 | /**
94 | * Returns the thread factory this HttpServer will obtain threads from.
95 | */
96 | public ThreadFactory getThreadFactory()
97 | {
98 | return factory;
99 | }
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | /**
108 | * Returns true if this server is active, false otherwise.
109 | */
110 | public boolean isActive()
111 | {
112 | return reactor.isActive();
113 | }
114 |
115 |
116 | /**
117 | * Waits for this server to shutdown.
118 | */
119 | public void await()
120 | {
121 | reactor.await();
122 | }
123 |
124 | /**
125 | * Wait for the specified amount of time for this server to shutdown. This
126 | * will return false if this returned because it timed out before the server
127 | * completely shutdown, otherwise it will return true.
128 | *
129 | * @param timeout the time to wait
130 | * @param unit the unit the timeout value is measured with
131 | * @return True if the server shutdown within the allotted time
132 | */
133 | public boolean await(long timeout, TimeUnit unit)
134 | {
135 | return reactor.await(timeout, unit);
136 | }
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 | private class RtspNioProvider implements NioProvider {
147 |
148 | public Thread newThread(Runnable r)
149 | {
150 | ThreadFactory factory = getThreadFactory();
151 | if(factory == null) {
152 | factory = Threads.newThreadFactory("RtspReactor", Thread.NORM_PRIORITY + 1, true);
153 | }
154 | return factory.newThread(r);
155 | }
156 |
157 | public void serviceException(Exception error)
158 | {
159 | log.warn("NIO Error reported", error);
160 | }
161 | }
162 |
163 | }
164 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/rtsp/RtspException.java:
--------------------------------------------------------------------------------
1 | package xpertss.rtsp;
2 |
3 | import java.net.ProtocolException;
4 |
5 | /**
6 | *
7 | */
8 | public class RtspException extends ProtocolException {
9 |
10 | public enum Type {
11 | Url, Network, Source, Carrier, Audio, Video, Authentication
12 | }
13 |
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/rtsp/RtspHandler.java:
--------------------------------------------------------------------------------
1 | package xpertss.rtsp;
2 |
3 | import java.io.IOException;
4 | import java.nio.ByteBuffer;
5 |
6 | /**
7 | * A handler receives events associated with the session. It is responsible
8 | * for coordinating the connection, the RTSP handshake, and processing or
9 | * forwarding data packets.
10 | */
11 | public interface RtspHandler {
12 |
13 | /**
14 | * Called to indicate the connection sequence has completed.
15 | */
16 | public void onConnect(RtspSession session);
17 |
18 | /**
19 | * Called to indicate the disconnection sequence has completed.
20 | */
21 | public void onClose(RtspSession session);
22 |
23 | /**
24 | * Called to indicate a failure on the session and to indicate
25 | * its closure.
26 | */
27 | public void onFailure(RtspSession session, Exception e);
28 |
29 |
30 | /**
31 | * Called to indicate data read from an interleaved channel.
32 | */
33 | public void onData(RtspSession session, int channel, ByteBuffer data) throws IOException;
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/rtsp/RtspMethod.java:
--------------------------------------------------------------------------------
1 | package xpertss.rtsp;
2 |
3 | import java.net.URI;
4 |
5 | /**
6 | */
7 | public enum RtspMethod {
8 |
9 | /**
10 | * The OPTIONS method represents a request for information about the
11 | * communication options available on the request/response chain
12 | * identified by the Request-URI. This method allows the client to
13 | * determine the options and/or requirements associated with a resource,
14 | * or the capabilities of a server, without implying a resource action
15 | * or initiating a resource retrieval.
16 | */
17 | Options(false),
18 |
19 | /**
20 | * When sent from client to server, ANNOUNCE posts the description of a
21 | * presentation or media object identified by the request URL to a
22 | * server. When sent from server to client, ANNOUNCE updates the session
23 | * description in real-time.
24 | */
25 | Announce(true) {
26 | @Override public RtspRequest createRequest(URI uri)
27 | {
28 | throw new UnsupportedOperationException("announce is not yet supported");
29 | }
30 | },
31 |
32 | /**
33 | * The DESCRIBE method retrieves the description of a presentation or
34 | * media object identified by the request URL from a server. It may use
35 | * the Accept header to specify the description formats that the client
36 | * understands. The server responds with a description of the requested
37 | * resource. The DESCRIBE reply-response pair constitutes the media
38 | * initialization phase of RTSP.
39 | */
40 | Describe(false),
41 |
42 | /**
43 | * The SETUP request for a URI specifies the transport mechanism to be
44 | * used for the streamed media.
45 | */
46 | Setup(false),
47 |
48 | /**
49 | * The PLAY method tells the server to start sending data via the
50 | * mechanism specified in SETUP. A client MUST NOT issue a PLAY request
51 | * until any outstanding SETUP requests have been acknowledged as
52 | * successful.
53 | */
54 | Play(false),
55 |
56 | /**
57 | * This method initiates recording a range of media data according to
58 | * the presentation description.
59 | */
60 | Record(false) {
61 | @Override public RtspRequest createRequest(URI uri)
62 | {
63 | throw new UnsupportedOperationException("record not yet supported");
64 | }
65 | },
66 |
67 | /**
68 | * The PAUSE request causes the stream delivery to be interrupted
69 | * (halted) temporarily. If the request URL names a stream, only
70 | * playback and recording of that stream is halted. For example, for
71 | * audio, this is equivalent to muting. If the request URL names a
72 | * presentation or group of streams, delivery of all currently active
73 | * streams within the presentation or group is halted.
74 | */
75 | Pause(false),
76 |
77 | /**
78 | * The TEARDOWN request stops the stream delivery for the given URI,
79 | * freeing the resources associated with it. If the URI is the
80 | * presentation URI for this presentation, any RTSP session identifier
81 | * associated with the session is no longer valid. Unless all transport
82 | * parameters are defined by the session description, a SETUP request
83 | * has to be issued before the session can be played again.
84 | */
85 | Teardown(false);
86 |
87 |
88 |
89 | private boolean supportsEntity;
90 |
91 | private RtspMethod(boolean supportsEntity)
92 | {
93 | this.supportsEntity = supportsEntity;
94 | }
95 |
96 |
97 | /**
98 | * Create a request using the current request method to the default uri
99 | * specified when the session was opened.
100 | */
101 | public RtspRequest createRequest()
102 | {
103 | return createRequest(null);
104 | }
105 |
106 | /**
107 | * Create a request using the current request method to the specified uri.
108 | */
109 | public RtspRequest createRequest(URI uri)
110 | {
111 | return new RtspRequest(this, uri);
112 | }
113 |
114 | /**
115 | * Allows an entity to be sent to the server.
116 | */
117 | public boolean supportsEntity() { return supportsEntity; }
118 |
119 |
120 | }
121 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/rtsp/RtspPlayer.java:
--------------------------------------------------------------------------------
1 | package xpertss.rtsp;
2 |
3 | import xpertss.SourceException;
4 | import xpertss.lang.Numbers;
5 | import xpertss.lang.Objects;
6 | import xpertss.lang.Range;
7 | import xpertss.lang.Strings;
8 | import xpertss.media.MediaChannel;
9 | import xpertss.media.MediaConsumer;
10 | import xpertss.mime.Header;
11 | import xpertss.mime.HeaderValue;
12 | import xpertss.mime.Headers;
13 | import xpertss.net.SocketOptions;
14 | import xpertss.sdp.MediaDescription;
15 | import xpertss.sdp.SessionDescription;
16 | import xpertss.sdp.SessionParser;
17 | import xpertss.utils.SafeProxy;
18 | import xpertss.utils.Utils;
19 |
20 | import java.io.IOException;
21 | import java.net.ProtocolException;
22 | import java.net.URI;
23 | import java.nio.ByteBuffer;
24 | import java.util.TreeSet;
25 |
26 | import static xpertss.net.OptionalSocketOptions.SO_TIMEOUT;
27 | import static xpertss.rtsp.RtspMethod.Describe;
28 | import static xpertss.rtsp.RtspMethod.Pause;
29 | import static xpertss.rtsp.RtspMethod.Play;
30 | import static xpertss.rtsp.RtspMethod.Setup;
31 | import static xpertss.rtsp.RtspMethod.Teardown;
32 | import static xpertss.rtsp.RtspState.*;
33 | import static xpertss.rtsp.RtspStatus.*;
34 |
35 | public class RtspPlayer {
36 |
37 | private final TreeSet channels = new TreeSet<>();
38 | private final MediaConsumer consumer;
39 | private final RtspClient client;
40 |
41 | private volatile RtspState state = Stopped;
42 | private RtspSession session;
43 | private int connectTimeout;
44 | private int readTimeout;
45 | private URI base;
46 |
47 |
48 |
49 | public RtspPlayer(RtspClient client, MediaConsumer consumer)
50 | {
51 | this.client = Objects.notNull(client);
52 | this.consumer = SafeProxy.newInstance(MediaConsumer.class,
53 | Objects.notNull(consumer));
54 | }
55 |
56 |
57 |
58 |
59 | public void setConnectTimeout(int connectTimeout)
60 | {
61 | this.connectTimeout = Numbers.gte(0, connectTimeout, "connectTimeout must not be negative");
62 | }
63 |
64 | public int getConnectTimeout()
65 | {
66 | return connectTimeout;
67 | }
68 |
69 |
70 |
71 |
72 | public void setReadTimeout(int readTimeout)
73 | {
74 | this.readTimeout = Numbers.gte(0, readTimeout, "readTimeout must not be negative");
75 | }
76 |
77 | public int getReadTimeout()
78 | {
79 | return readTimeout;
80 | }
81 |
82 |
83 |
84 |
85 |
86 | public SessionDescription getSessionDescription()
87 | {
88 | return getSessionDescription(session);
89 | }
90 |
91 |
92 |
93 |
94 | public RtspState getState()
95 | {
96 | return state;
97 | }
98 |
99 |
100 |
101 |
102 | /**
103 | * Connect to the remote media server and setup the media streams.
104 | */
105 | public void start(URI uri)
106 | {
107 | if(state == Stopped) {
108 | state = Activating;
109 | session = client.open(new RtspPlaybackHandler(), uri);
110 |
111 | SocketOptions.set(session, SO_TIMEOUT, readTimeout);
112 |
113 | session.connect(connectTimeout);
114 | }
115 | }
116 |
117 | /**
118 | * Play the configured media streams.
119 | */
120 | public void play()
121 | {
122 | if(state == Paused) {
123 | state = Activating;
124 | RtspRequest request = Play.createRequest(base);
125 | Headers headers = request.getHeaders();
126 | setSessionId(session, headers);
127 | headers.setHeader("Range", "npt=0.000-");
128 | session.execute(request, new DefaultResponseHandler() {
129 | @Override public void onOkResponse(RtspSession session, RtspResponse response) throws IOException {
130 | state = Active;
131 | }
132 | });
133 | }
134 | }
135 |
136 | /**
137 | * Pause the configured media streams.
138 | */
139 | public void pause()
140 | {
141 | if(state == Active) {
142 | state = Pausing;
143 | RtspRequest request = Pause.createRequest(base);
144 | Headers headers = request.getHeaders();
145 | setSessionId(session, headers);
146 | session.execute(request, new DefaultResponseHandler() {
147 | @Override public void onOkResponse(RtspSession session, RtspResponse response) throws IOException {
148 | state = Paused;
149 | }
150 | });
151 | }
152 | }
153 |
154 | /**
155 | * Stop playback, tear down the configured streams and disconnect.
156 | */
157 | public void stop()
158 | {
159 | if(!Objects.isOneOf(state, Stopped, Stopping)) {
160 | state = Stopping;
161 | RtspRequest request = Teardown.createRequest(base);
162 | Headers headers = request.getHeaders();
163 | setSessionId(session, headers);
164 | headers.setHeader("Connection", "close");
165 | session.execute(request, new DefaultResponseHandler() {
166 | @Override public void onOkResponse(RtspSession session, RtspResponse response) throws IOException {
167 | consumer.destroyChannels();
168 | session.close();
169 | }
170 | });
171 | session = null;
172 | }
173 | }
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 | private static final String TRANSPORT = "RTP/AVP/TCP;unicast;interleaved=%d-%d";
182 |
183 | private void setupChannel(RtspSession session) throws IOException
184 | {
185 | final MediaChannel channel = channels.pollFirst();
186 | if(channel == null) {
187 | startPlayback(session);
188 | } else {
189 | String control = channel.getControl();
190 | URI target = (Strings.isEmpty(control)) ? base : base.resolve(control);
191 | RtspRequest request = Setup.createRequest(target);
192 | Headers headers = request.getHeaders();
193 |
194 | final Range channels = channel.getChannels();
195 | headers.setHeader("Transport", String.format(TRANSPORT,
196 | channels.getLower(),
197 | channels.getUpper()));
198 | setSessionId(session, headers);
199 |
200 | session.execute(request, new DefaultResponseHandler() {
201 | @Override
202 | public void onOkResponse(RtspSession session, RtspResponse response) throws IOException {
203 | consumer.createChannel(channel);
204 | session.setAttribute("session.id", Utils.getBaseHeader(response, "Session"));
205 | setupChannel(session);
206 | }
207 | });
208 | }
209 | }
210 |
211 |
212 |
213 |
214 | private void startPlayback(RtspSession session) throws IOException
215 | {
216 | RtspRequest request = Play.createRequest(base);
217 | Headers headers = request.getHeaders();
218 | setSessionId(session, headers);
219 | headers.setHeader("Range", "npt=0.000-");
220 | session.execute(request, new DefaultResponseHandler() {
221 | @Override public void onOkResponse(RtspSession session, RtspResponse response) throws IOException {
222 | state = Active;
223 | }
224 | });
225 | }
226 |
227 |
228 |
229 |
230 | private static void setSessionId(RtspSession session, Headers headers)
231 | {
232 | String sessionId = (String) session.getAttribute("session.id");
233 | if(!Strings.isEmpty(sessionId)) headers.setHeader("Session", sessionId);
234 | }
235 |
236 | private static SessionDescription getSessionDescription(RtspSession session)
237 | {
238 | return (session == null) ? null : (SessionDescription)
239 | session.getAttribute("session.description");
240 | }
241 |
242 |
243 | private class RtspPlaybackHandler implements RtspHandler {
244 |
245 | @Override
246 | public void onConnect(RtspSession session)
247 | {
248 | RtspRequest request = Describe.createRequest();
249 | Headers headers = request.getHeaders();
250 | headers.setHeader("Accept", "application/sdp");
251 | session.execute(request, new DefaultResponseHandler() {
252 | @Override public void onOkResponse(RtspSession session, RtspResponse response) throws IOException {
253 | Headers headers = response.getHeaders();
254 |
255 | String baseUri = Headers.toString(headers.getHeader("Content-Base"));
256 | base = (Strings.isEmpty(baseUri)) ? session.getResource() : URI.create(baseUri);
257 |
258 | String contentType = Headers.toString(headers.getHeader("Content-Type"));
259 | if (Strings.equal("application/sdp", contentType)) {
260 | SessionParser parser = new SessionParser();
261 | SessionDescription sessionDescription = parser.parse(response.getEntityBody());
262 | session.setAttribute("session.description", sessionDescription);
263 |
264 | MediaDescription[] medias = consumer.select(sessionDescription);
265 | if(Objects.isEmpty(medias)) throw new ProtocolException("missing media resource");
266 | channels.clear();
267 | for(int i = 0; i < medias.length; i++) {
268 | channels.add(new MediaChannel(medias[i], new Range<>(i * 2, i * 2 + 1)));
269 | }
270 |
271 | setupChannel(session);
272 | } else {
273 | throw new ProtocolException("unexpected entity type received");
274 | }
275 | }
276 | });
277 | }
278 |
279 | @Override
280 | public void onClose(RtspSession session)
281 | {
282 | state = Stopped;
283 | }
284 |
285 | @Override
286 | public void onFailure(RtspSession session, Exception e)
287 | {
288 | state = Stopped;
289 | consumer.handle(e);
290 | }
291 |
292 |
293 |
294 |
295 | @Override
296 | public void onData(RtspSession session, int channelId, ByteBuffer data) throws IOException
297 | {
298 | consumer.consume(channelId, data);
299 | }
300 |
301 |
302 | }
303 |
304 | private static class DefaultResponseHandler implements RtspResponseHandler {
305 |
306 | @Override public void onResponse(RtspSession session, RtspResponse response) throws IOException {
307 | if(response.getStatus() != Ok)
308 | throw new SourceException(response.getStatus().getCode(), response.getStatusReason());
309 | onOkResponse(session, response);
310 | }
311 |
312 | protected void onOkResponse(RtspSession session, RtspResponse response) throws IOException
313 | {
314 | }
315 | }
316 |
317 | }
318 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/rtsp/RtspRequest.java:
--------------------------------------------------------------------------------
1 | package xpertss.rtsp;
2 |
3 | import xpertss.io.Buffers;
4 | import xpertss.lang.Objects;
5 | import xpertss.lang.Strings;
6 | import xpertss.mime.Headers;
7 | import xpertss.nio.NioWriter;
8 |
9 | import java.io.IOException;
10 | import java.net.URI;
11 | import java.nio.ByteBuffer;
12 |
13 | import static xpertss.io.Charsets.*;
14 | import static xpertss.rtsp.RtspMethod.*;
15 |
16 |
17 | /**
18 | */
19 | public class RtspRequest {
20 |
21 | private final Headers headers = new Headers(Headers.Type.Rtsp);
22 | private final RtspMethod method;
23 | private final URI target;
24 |
25 | RtspRequest(RtspMethod method, URI target)
26 | {
27 | if(!Objects.isOneOf(method, Options, Describe, Setup, Play, Pause, Teardown))
28 | throw new UnsupportedOperationException("method not yet supported");
29 | this.method = Objects.notNull(method, "method");
30 | this.target = target;
31 | }
32 |
33 |
34 |
35 |
36 | public RtspMethod getMethod()
37 | {
38 | return method;
39 | }
40 |
41 | public Headers getHeaders()
42 | {
43 | return headers;
44 | }
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | NioWriter createWriter(URI uri)
53 | {
54 | // TODO Check that target and uri have same authority
55 | return new RequestWriter(uri);
56 | }
57 |
58 | private class RequestWriter implements NioWriter {
59 |
60 | private final URI uri;
61 | private ByteBuffer encoded;
62 | private RequestWriter(URI uri)
63 | {
64 | this.uri = Objects.notNull(uri);
65 | }
66 |
67 | @Override
68 | public boolean writeTo(ByteBuffer dst) throws IOException
69 | {
70 | if(encoded == null) encoded = encode();
71 | Buffers.copyTo(encoded, dst);
72 | return !encoded.hasRemaining();
73 | }
74 |
75 | private ByteBuffer encode() throws IOException
76 | {
77 | StringBuilder builder = new StringBuilder();
78 | builder.append(Strings.toUpper(method.name())).append(" ");
79 | builder.append((target == null) ? uri : target);
80 | builder.append(" ").append("RTSP/1.0").append("\r\n");
81 | builder.append(headers);
82 | builder.append("\r\n");
83 | return US_ASCII.encode(builder.toString());
84 | }
85 |
86 | }
87 |
88 |
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/rtsp/RtspResponse.java:
--------------------------------------------------------------------------------
1 | package xpertss.rtsp;
2 |
3 | import xpertss.lang.Objects;
4 | import xpertss.lang.Strings;
5 | import xpertss.mime.Headers;
6 |
7 | import java.io.ByteArrayInputStream;
8 | import java.io.InputStream;
9 | import java.util.List;
10 |
11 | /**
12 | */
13 | public class RtspResponse {
14 |
15 | private InputStream entity = new ByteArrayInputStream(new byte[0]);
16 |
17 | private final String reason;
18 | private final Headers headers;
19 | private final RtspStatus status;
20 |
21 | RtspResponse(RtspStatus status, String reason, Headers headers)
22 | {
23 | this.status = Objects.notNull(status);
24 | this.reason = Strings.notEmpty(reason, "reason mus tbe defined");
25 | this.headers = Objects.notNull(headers);
26 | }
27 |
28 |
29 | public RtspStatus getStatus()
30 | {
31 | return status;
32 | }
33 |
34 | public String getStatusReason() { return reason; }
35 |
36 | public Headers getHeaders() { return headers; }
37 |
38 |
39 | public InputStream getEntityBody()
40 | {
41 | return entity;
42 | }
43 |
44 | public String toString()
45 | {
46 | return String.format("RTSP/1.0 %d %s", status.getCode(), reason);
47 | }
48 |
49 |
50 |
51 | RtspResponse withEntity(InputStream entity)
52 | {
53 | this.entity = Objects.ifNull(entity, this.entity);
54 | return this;
55 | }
56 |
57 | static RtspResponse create(List lines)
58 | {
59 | String[] status = lines.remove(0).split("\\s+", 3);
60 | return new RtspResponse(RtspStatus.valueOf(Integer.parseInt(status[1])), status[2], Headers.create(lines));
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/rtsp/RtspResponseHandler.java:
--------------------------------------------------------------------------------
1 | package xpertss.rtsp;
2 |
3 | import java.io.IOException;
4 |
5 | /**
6 | */
7 | public interface RtspResponseHandler {
8 |
9 | public void onResponse(RtspSession session, RtspResponse response) throws IOException;
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/rtsp/RtspSession.java:
--------------------------------------------------------------------------------
1 | package xpertss.rtsp;
2 |
3 |
4 | import org.slf4j.Logger;
5 | import org.slf4j.LoggerFactory;
6 | import xpertss.io.Buffers;
7 | import xpertss.io.NIOUtils;
8 | import xpertss.lang.Booleans;
9 | import xpertss.lang.Integers;
10 | import xpertss.lang.Numbers;
11 | import xpertss.lang.Objects;
12 | import xpertss.lang.Strings;
13 | import xpertss.mime.Headers;
14 | import xpertss.nio.Checkable;
15 | import xpertss.nio.ConnectHandler;
16 | import xpertss.nio.DataHandler;
17 | import xpertss.nio.NioAction;
18 | import xpertss.nio.NioReader;
19 | import xpertss.nio.NioService;
20 | import xpertss.nio.NioSession;
21 | import xpertss.nio.NioStats;
22 | import xpertss.nio.NioWriter;
23 | import xpertss.nio.ReadyState;
24 | import xpertss.nio.Selectable;
25 | import xpertss.utils.UserAgent;
26 | import xpertss.utils.Utils;
27 |
28 | import java.io.EOFException;
29 | import java.io.IOException;
30 | import java.net.ConnectException;
31 | import java.net.ProtocolException;
32 | import java.net.SocketAddress;
33 | import java.net.SocketOption;
34 | import java.net.SocketTimeoutException;
35 | import java.net.URI;
36 | import java.nio.ByteBuffer;
37 | import java.nio.CharBuffer;
38 | import java.nio.channels.SelectableChannel;
39 | import java.nio.channels.SelectionKey;
40 | import java.nio.channels.Selector;
41 | import java.nio.channels.SocketChannel;
42 | import java.util.ArrayList;
43 | import java.util.Collections;
44 | import java.util.Deque;
45 | import java.util.HashSet;
46 | import java.util.List;
47 | import java.util.Queue;
48 | import java.util.Set;
49 | import java.util.concurrent.ConcurrentHashMap;
50 | import java.util.concurrent.ConcurrentLinkedDeque;
51 | import java.util.concurrent.ConcurrentLinkedQueue;
52 | import java.util.concurrent.ConcurrentMap;
53 | import java.util.concurrent.TimeUnit;
54 |
55 | import static java.nio.channels.SelectionKey.*;
56 | import static xpertss.net.OptionalSocketOptions.SO_TIMEOUT;
57 | import static xpertss.nio.ReadyState.*;
58 | import static xpertss.rtsp.RtspMethod.*;
59 |
60 | /**
61 | * This RtpSession impl is designed to support a streaming player not a
62 | * streaming producer.
63 | */
64 | public class RtspSession implements NioSession, DataHandler, ConnectHandler, Checkable {
65 |
66 |
67 | private final ConcurrentMap attributes = new ConcurrentHashMap<>();
68 | private final Deque readers = new ConcurrentLinkedDeque<>();
69 | private final Queue writeQueue = new ConcurrentLinkedQueue<>();
70 | private final ByteBuffer writeBuf = ByteBuffer.allocate(8192);
71 | private final ByteBuffer readBuf = ByteBuffer.allocate(8192);
72 | private final long createTime = System.currentTimeMillis();
73 | private final ReadManager readManager = new ReadManager();
74 | private final Logger log = LoggerFactory.getLogger(getClass());
75 | private final NioStats write = new NioStats();
76 | private final NioStats read = new NioStats();
77 | private final SocketAddress address;
78 | private final SocketChannel channel;
79 | private final RtspHandler handler;
80 | private final NioService service;
81 | private final String userAgent;
82 | private final URI target;
83 |
84 | private volatile ReadyState readyState = Open;
85 | private Set> valid;
86 | private long timeoutConnection;
87 | private NioReader lastReader;
88 | private int readTimeout= 0;
89 | private int sequence = 0;
90 |
91 |
92 | RtspSession(NioService service, RtspHandler handler, URI target, UserAgent userAgent)
93 | {
94 | this.service = Objects.notNull(service);
95 | this.handler = Objects.notNull(handler);
96 | this.target = Objects.notNull(target);
97 | this.address = Utils.createSocketAddress(target, 554);
98 | this.channel = NIOUtils.openTcpSocket(false);
99 | this.userAgent = Objects.notNull(userAgent).toString();
100 | }
101 |
102 |
103 |
104 | // Rtsp functions
105 |
106 | public URI getResource()
107 | {
108 | return target;
109 | }
110 |
111 | public void execute(RtspRequest request, RtspResponseHandler handler)
112 | {
113 | Headers headers = request.getHeaders();
114 | headers.setHeader("CSeq", Integer.toString(++sequence));
115 | headers.setIfNotSet("User-Agent", userAgent);
116 | writeQueue.offer(request.createWriter(target));
117 | readers.offer(new ResponseReader(handler, request.getMethod()));
118 | service.execute(new RegisterAction(OP_WRITE));
119 | }
120 |
121 |
122 | public void write(int channelId, ByteBuffer data)
123 | {
124 | Numbers.within(0, 255, channelId, "channelId outside range 0 - 255");
125 | Numbers.within(0, 65545, data.remaining(), "data buffer is too large");
126 | writeQueue.offer(new ChannelWriter(data, channelId));
127 | service.execute(new RegisterAction(OP_WRITE));
128 | }
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 | // Base session functions
139 |
140 | /**
141 | * Connect this session to the target endpoint. Timeout the attempt after
142 | * the given timeout period measured in milliseconds has passed.
143 | */
144 | public void connect(int timeout)
145 | {
146 | if(channel.isOpen()) {
147 | readyState = Connecting;
148 | this.timeoutConnection = Utils.computeTimeout(timeout);
149 | service.execute(new ConnectAction(channel, address));
150 | }
151 | }
152 |
153 | /**
154 | * Close the session.
155 | */
156 | public void close()
157 | {
158 | if(channel.isOpen()) {
159 | readyState = Closing;
160 | service.execute(new CloseAction());
161 | }
162 | }
163 |
164 |
165 |
166 |
167 |
168 | @Override
169 | public ReadyState getReadyState()
170 | {
171 | return readyState;
172 | }
173 |
174 |
175 |
176 |
177 |
178 |
179 | @Override
180 | public SocketAddress getRemoteAddress()
181 | {
182 | return channel.socket().getRemoteSocketAddress();
183 | }
184 |
185 | @Override
186 | public SocketAddress getLocalAddress()
187 | {
188 | return channel.socket().getLocalSocketAddress();
189 | }
190 |
191 | @Override
192 | public SocketAddress getServiceAddress()
193 | {
194 | return address;
195 | }
196 |
197 |
198 |
199 |
200 |
201 | @Override
202 | public RtspSession setOption(SocketOption option, T value) throws IOException
203 | {
204 | if(option == SO_TIMEOUT) {
205 | this.readTimeout = Utils.maxIfZero(Numbers.gte(0, (Integer) value, "timeout must not be negative"));
206 | } else if(channel.supportedOptions().contains(option)) {
207 | channel.setOption(option, value);
208 | }
209 | return this;
210 | }
211 |
212 |
213 | @Override
214 | public T getOption(SocketOption option) throws IOException
215 | {
216 | if(option == SO_TIMEOUT) {
217 | return option.type().cast(this.readTimeout);
218 | } else if(channel.supportedOptions().contains(option)) {
219 | return channel.getOption(option);
220 | }
221 | throw new UnsupportedOperationException();
222 | }
223 |
224 |
225 | @Override
226 | public Set> supportedOptions()
227 | {
228 | if(valid == null) {
229 | Set> set = new HashSet<>();
230 | set.addAll(channel.supportedOptions());
231 | set.add(SO_TIMEOUT);
232 | valid = Collections.unmodifiableSet(set);
233 | }
234 | return valid;
235 | }
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 | @Override
249 | public long getCreationTime()
250 | {
251 | return createTime;
252 | }
253 |
254 | @Override
255 | public long getLastIoTime()
256 | {
257 | return Math.max(read.getTime(), write.getTime());
258 | }
259 |
260 | @Override
261 | public long getLastReadTime()
262 | {
263 | return read.getTime();
264 | }
265 |
266 | @Override
267 | public long getLastWriteTime()
268 | {
269 | return write.getTime();
270 | }
271 |
272 |
273 |
274 |
275 |
276 | @Override
277 | public long getBytesWritten()
278 | {
279 | return write.getCount();
280 | }
281 |
282 | @Override
283 | public long getBytesRead()
284 | {
285 | return read.getCount();
286 | }
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 | @Override
295 | public Object getAttribute(String key)
296 | {
297 | return attributes.get(key);
298 | }
299 |
300 | @Override
301 | public void setAttribute(String key, Object value)
302 | {
303 | attributes.put(key, value);
304 | }
305 |
306 | @Override
307 | public void removeAttribute(String key)
308 | {
309 | attributes.remove(key);
310 | }
311 |
312 | @Override
313 | public boolean hasAttribute(String key)
314 | {
315 | return attributes.containsKey(key);
316 | }
317 |
318 | @Override
319 | public Set getAttributeKeys()
320 | {
321 | return Collections.unmodifiableSet(attributes.keySet());
322 | }
323 |
324 |
325 |
326 |
327 |
328 |
329 | @Override
330 | public void handleConnect() throws IOException
331 | {
332 | channel.finishConnect();
333 | service.execute(new UnregisterAction(OP_CONNECT));
334 | handler.onConnect(this);
335 | readyState = Connected;
336 | service.execute(new RegisterAction(OP_READ));
337 | }
338 |
339 | @Override
340 | public void handleRead() throws IOException
341 | {
342 | int result = channel.read(readBuf);
343 | if(result < 0) {
344 | throw new EOFException("peer closed connection");
345 | } else {
346 | read.record(result);
347 | readBuf.flip();
348 | while(readBuf.hasRemaining()) {
349 | if(lastReader != null) {
350 | if(lastReader.readFrom(readBuf)) {
351 | lastReader = null;
352 | }
353 | } else if(readBuf.get(readBuf.position()) == 0x24) {
354 | if(readBuf.remaining() < 4) break;
355 | lastReader = new DataReader();
356 | if(lastReader.readFrom(readBuf)) {
357 | lastReader = null;
358 | }
359 | } else {
360 | // TODO We need to be prepared for a spontaneous RTSP Response block
361 | // while reading channelized data. This might for example be the sever
362 | // telling us it is timing out our session due to lack of keep alive.
363 | lastReader = readers.poll();
364 | if(lastReader != null) {
365 | if (lastReader.readFrom(readBuf)) {
366 | lastReader = null;
367 | }
368 | } else {
369 | // We probably should have some sort of ResponseReader here
370 | String str = Buffers.toHexString(readBuf, readBuf.position(), Math.min(readBuf.remaining(), 10));
371 | log.error("Unexpected data: " + str);
372 | throw new ProtocolException("unexpected read received");
373 | }
374 | }
375 | }
376 | readBuf.compact();
377 | }
378 | }
379 |
380 | @Override
381 | public void handleWrite() throws IOException
382 | {
383 | NioWriter writer = writeQueue.peek();
384 | if(writer != null) {
385 | if(writer.writeTo(writeBuf)) writeQueue.remove();
386 | }
387 | writeBuf.flip();
388 | if(writeBuf.hasRemaining()) {
389 | write.record(channel.write(writeBuf));
390 | } else if(writeQueue.isEmpty()) {
391 | service.execute(new UnregisterAction(SelectionKey.OP_WRITE));
392 | }
393 | writeBuf.compact();
394 | }
395 |
396 |
397 | @Override
398 | public void handleCheckup() throws IOException
399 | {
400 | int timeout = Utils.maxIfZero(readTimeout);
401 | if(readyState == Connecting && System.nanoTime() >= timeoutConnection) {
402 | throw new ConnectException("connection timed out");
403 | } else if(readyState == Connected) {
404 | ResponseReader reader = readers.peek();
405 | if(reader != null && reader.getWaitingTime() >= timeout) {
406 | throw new SocketTimeoutException("response timed out");
407 | } else if(readManager.isReaderActive()) {
408 | if(System.currentTimeMillis() - read.getTime() >= timeout) {
409 | throw new SocketTimeoutException("read timed out");
410 | }
411 | }
412 | }
413 | }
414 |
415 |
416 | @Override
417 | public SelectableChannel getChannel()
418 | {
419 | return channel;
420 | }
421 |
422 | @Override
423 | public void shutdown(Exception ex)
424 | {
425 | NIOUtils.close(channel);
426 | try {
427 | if (ex == null) handler.onClose(this);
428 | else handler.onFailure(this, ex);
429 | } finally {
430 | readyState = Closed;
431 | }
432 | }
433 |
434 |
435 |
436 |
437 | private class ConnectAction implements NioAction {
438 |
439 | private final SocketAddress address;
440 | private final SocketChannel socket;
441 |
442 | public ConnectAction(SocketChannel socket, SocketAddress address)
443 | {
444 | this.socket = Objects.notNull(socket, "socket");
445 | this.address = Objects.notNull(address, "address");
446 | }
447 |
448 | public void execute(Selector selector) throws IOException
449 | {
450 | SelectableChannel channel = getChannel();
451 | if(channel != null && channel.isOpen()) {
452 | SelectionKey sk = channel.keyFor(selector);
453 | if(sk == null) {
454 | channel.register(selector, OP_CONNECT, RtspSession.this);
455 | socket.connect(address);
456 | }
457 | }
458 | }
459 |
460 | public Selectable getSelectable()
461 | {
462 | return RtspSession.this;
463 | }
464 |
465 | }
466 |
467 | private class CloseAction implements NioAction {
468 |
469 | public void execute(Selector selector)
470 | {
471 | SelectableChannel channel = getChannel();
472 | if(channel != null && channel.isOpen()) {
473 | shutdown(null);
474 | }
475 | }
476 |
477 | public Selectable getSelectable()
478 | {
479 | return RtspSession.this;
480 | }
481 |
482 | }
483 |
484 | private class RegisterAction implements NioAction {
485 |
486 | private int ops;
487 |
488 | private RegisterAction(int ops)
489 | {
490 | this.ops = ops;
491 | }
492 |
493 | public void execute(Selector selector) throws IOException
494 | {
495 | SelectableChannel channel = getChannel();
496 | if(channel != null && channel.isOpen()) {
497 | SelectionKey sk = channel.keyFor(selector);
498 | if(sk == null) {
499 | channel.register(selector, ops, RtspSession.this);
500 | } else {
501 | sk.interestOps(sk.interestOps() | ops);
502 | }
503 | }
504 | }
505 |
506 | public Selectable getSelectable()
507 | {
508 | return RtspSession.this;
509 | }
510 | }
511 |
512 | private class UnregisterAction implements NioAction {
513 |
514 | private int ops;
515 |
516 | private UnregisterAction(int ops)
517 | {
518 | this.ops = ops;
519 | }
520 |
521 | public void execute(Selector selector)
522 | {
523 | SelectableChannel channel = getChannel();
524 | if(channel != null) {
525 | SelectionKey sk = channel.keyFor(selector);
526 | if(sk != null && sk.isValid()) {
527 | sk.interestOps(sk.interestOps() & ~ops);
528 | }
529 | }
530 | }
531 |
532 | public Selectable getSelectable()
533 | {
534 | return RtspSession.this;
535 | }
536 |
537 | }
538 |
539 |
540 |
541 |
542 |
543 | private class ChannelWriter implements NioWriter {
544 |
545 | private ByteBuffer data;
546 | private ChannelWriter(ByteBuffer src, int channelId)
547 | {
548 | data = ByteBuffer.allocate(src.remaining() + 4);
549 | data.put((byte)0x24).put((byte)channelId).putShort((short)src.remaining());
550 | Buffers.copyTo(src, data);
551 | data.flip();
552 | }
553 |
554 | @Override
555 | public boolean writeTo(ByteBuffer dst) throws IOException
556 | {
557 | Buffers.copyTo(data, dst);
558 | return !(data.hasRemaining());
559 | }
560 | }
561 |
562 |
563 |
564 | private class ResponseReader implements NioReader {
565 |
566 | private final CharBuffer lineBuffer = CharBuffer.allocate(2048); // limit header/request line length to 2k
567 | private final List lines = new ArrayList<>();
568 | private final long createTime = System.nanoTime();
569 | private final RtspResponseHandler handler;
570 | private final RtspMethod method;
571 |
572 | private RtspResponse response;
573 | private ByteBuffer entity;
574 |
575 | public ResponseReader(RtspResponseHandler handler, RtspMethod method)
576 | {
577 | this.handler = Objects.notNull(handler);
578 | this.method = Objects.notNull(method);
579 | }
580 |
581 | public long getWaitingTime()
582 | {
583 | return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - createTime);
584 | }
585 |
586 |
587 | @Override
588 | public boolean readFrom(ByteBuffer src) throws IOException
589 | {
590 | if(response == null) response = readResponse(src);
591 | if (response != null && readEntity(src)) {
592 | if(response.getStatus() == RtspStatus.Ok)
593 | readManager.eval(method, response.getHeaders());
594 | handler.onResponse(RtspSession.this, response);
595 | return true;
596 | }
597 | return false;
598 | }
599 |
600 | private RtspResponse readResponse(ByteBuffer src) throws IOException
601 | {
602 | while(readLine(src, lineBuffer)) {
603 | if(lineBuffer.position() == 0) {
604 | if(lines.isEmpty()) throw new IOException("premature end of response");
605 | return RtspResponse.create(lines);
606 | } else if(lines.size() < 30) {
607 | lines.add(Utils.consume(lineBuffer, false));
608 | } else {
609 | throw new ProtocolException("max number of lines exceeded");
610 | }
611 | }
612 | return null;
613 | }
614 |
615 | private boolean readEntity(ByteBuffer src) throws IOException
616 | {
617 | if(entity == null) {
618 | long length = response.getHeaders().getContentLength();
619 | if(length > 8196L) throw new ProtocolException("response entity too large");
620 | if(length <= 0) return true;
621 | entity = ByteBuffer.allocate(Integers.safeCast(length));
622 | }
623 | Buffers.copyTo(src, entity);
624 | if (entity.hasRemaining()) return false;
625 | response = response.withEntity(Buffers.newInputStream(entity));
626 | return true;
627 | }
628 |
629 |
630 | private boolean readLine(ByteBuffer src, CharBuffer dst)
631 | {
632 | while(src.hasRemaining()) {
633 | char c = (char) (src.get() & 0xff);
634 | if(c == '\n') {
635 | return true;
636 | } else if(c != '\r') {
637 | dst.append(c);
638 | }
639 | }
640 | return false;
641 | }
642 |
643 |
644 | }
645 |
646 | private class DataReader implements NioReader {
647 |
648 | private ByteBuffer data;
649 | private int channelId;
650 |
651 | @Override
652 | public boolean readFrom(ByteBuffer src) throws IOException
653 | {
654 | if(data == null) {
655 | if (src.get() != 0x24) throw new ProtocolException("expected interleaved data");
656 | channelId = src.get();
657 | int len = src.getShort();
658 | data = ByteBuffer.allocate(len);
659 | }
660 | Buffers.copyTo(src, data);
661 | if (data.hasRemaining()) return false;
662 | data.flip();
663 | handler.onData(RtspSession.this, channelId, data.asReadOnlyBuffer());
664 | return true;
665 | }
666 | }
667 |
668 |
669 |
670 | private class ReadManager {
671 |
672 | private final ConcurrentMap channels = new ConcurrentHashMap<>();
673 | private int count;
674 |
675 |
676 | public void eval(RtspMethod method, Headers response)
677 | {
678 | if(method == Setup) {
679 | String transport = Headers.toString(response.getHeader("Transport"));
680 | if(Strings.contains(transport, "interleaved")) {
681 | String sessionId = Headers.toString(response.getHeader("Session"));
682 | if(sessionId != null) channels.put(sessionId, false);
683 | }
684 | } else if(method == Play) {
685 | String sessionId = Headers.toString(response.getHeader("Session"));
686 | if(sessionId != null && channels.replace(sessionId, false, true)) count++;
687 | } else if(method == Pause) {
688 | String sessionId = Headers.toString(response.getHeader("Session"));
689 | if(sessionId != null && channels.replace(sessionId, true, false)) count--;
690 | } else if(method == Teardown) {
691 | String sessionId = Headers.toString(response.getHeader("Session"));
692 | if(Booleans.isTrue(channels.remove(sessionId))) count--;
693 | }
694 | }
695 |
696 | public boolean isReaderActive()
697 | {
698 | return count > 0;
699 | }
700 |
701 | }
702 |
703 | }
704 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/rtsp/RtspState.java:
--------------------------------------------------------------------------------
1 | package xpertss.rtsp;
2 |
3 | /**
4 | * An enumeration of Rtsp States.
5 | */
6 | public enum RtspState {
7 |
8 | /**
9 | * The player or producer is not connected.
10 | */
11 | Stopped,
12 |
13 | /**
14 | * The player or producer are in the process of inactivating
15 | */
16 | Pausing,
17 |
18 | /**
19 | * The player or producer is connected and setup but not
20 | * actively playing or recording media.
21 | */
22 | Paused,
23 |
24 | /**
25 | * The player or producer are in the process of starting
26 | * playback or recording.
27 | */
28 | Activating,
29 |
30 | /**
31 | * The player or producer are actively playing or recording
32 | * media content.
33 | */
34 | Active,
35 |
36 | /**
37 | * The player or producer is in the process of tearing down
38 | * the session and disconnecting.
39 | */
40 | Stopping
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/rtsp/RtspStatus.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright XpertSoftware All rights reserved.
3 | *
4 | * Date: 3/9/11 1:37 PM
5 | */
6 | package xpertss.rtsp;
7 |
8 | public enum RtspStatus {
9 |
10 | Continue(100, "Continue", false),
11 |
12 | Ok(200, "Ok", true),
13 | Created(201, "Created", true),
14 | LowOnStorageSpace(250, "Low on Storage Space", true),
15 |
16 |
17 | MultipleChoices(300, "Multiple Choices", true),
18 | MovedPermanently(301, "Moved Permanently", true),
19 | MovedTemporarily(302, "Moved Temporarily", true), // HTTP/1.0
20 | SeeOther(303, "See Other", true),
21 | NotModified(304, "Not Modified", false),
22 | UseProxy(305, "Use Proxy", true),
23 |
24 |
25 | BadRequest(400, "Bad Request", true),
26 | Unauthorized(401, "Unauthorized", true),
27 | PaymentRequired(402, "Payment Required", true),
28 | Forbidden(403, "Forbidden", true),
29 | NotFound(404, "Not Found", true),
30 | MethodNotAllowed(405, "Method Not Allowed", true),
31 | NotAcceptable(406, "Not Acceptable", true),
32 | ProxyAuthenticationRequired(407, "Proxy Authentication Required", true),
33 | RequestTimeout(408, "Request Timeout", true),
34 | Gone(410, "Gone", true),
35 |
36 | LengthRequired(411, "Length Required", true),
37 | PreconditionFailed(412, "Precondition Failed", true),
38 | RequestEntityTooLarge(413, "Request Entity Too Large", true),
39 | RequestUriTooLong(414, "Request-URI Too Long", true),
40 | UnsupportedMediaType(415, "Unsupported Media Type", true),
41 |
42 | ParameterNotUnderstood(451, "Parameter Not Understood", true),
43 | ConferenceNotFound(452, "Conference Not Found", true),
44 | NotEnoughBandwidth(453, "Not Enough Bandwidth", true),
45 | SessionNotFound(454, "Session Not Found", true),
46 | MethodNotValidInThisState(455, "Method Not Valid in This State", true),
47 | HeaderFieldNotValid(456, "Header Field Not Valid for Resource", true),
48 | InvalidRange(457, "Invalid Range", true),
49 | ParameterIsReadOnly(458, "Parameter Is Read-Only", true),
50 | UnsupportedTransport(461, "Unsupported Transport", true),
51 | DestinationUnreachable(462, "Destination Unreachable", true),
52 |
53 |
54 | InternalServerError(500, "Internal Server Error", true),
55 | NotImplemented(501, "Not Implemented", true),
56 | BadGateway(502, "Bad Gateway", true),
57 | ServiceUnavailable(503, "Service Unavailable", true),
58 | GatewayTimeout(504, "Gateway Timeout", true),
59 | RTSPVersionNotSupported(505, "RTSP Version Not Supported", true),
60 | OptionNotSupported(551, "Option Not Supported", true); // WebDav
61 |
62 |
63 | private int code;
64 | private String reason;
65 | private boolean allowsEntity;
66 |
67 | private RtspStatus(int code, String reason, boolean allowsEntity)
68 | {
69 | this.code = code;
70 | this.reason = reason;
71 | this.allowsEntity = allowsEntity;
72 | }
73 |
74 | public int getCode()
75 | {
76 | return code;
77 | }
78 |
79 | public String getReason()
80 | {
81 | return reason;
82 | }
83 |
84 | public boolean allowsEntity()
85 | {
86 | return allowsEntity;
87 | }
88 |
89 | @Override
90 | public String toString()
91 | {
92 | return String.format("%d %s", getCode(), getReason());
93 | }
94 |
95 |
96 | public boolean isInformational()
97 | {
98 | return code >= 100 && code < 200;
99 | }
100 |
101 | public boolean isSuccess()
102 | {
103 | return code >= 200 && code < 300;
104 | }
105 |
106 | public boolean isRedirection()
107 | {
108 | return code >= 300 && code < 400;
109 | }
110 |
111 | public boolean isClientError()
112 | {
113 | return code >= 400 && code < 500;
114 | }
115 |
116 | public boolean isServerError()
117 | {
118 | return code >= 500;
119 | }
120 |
121 |
122 |
123 | public static RtspStatus valueOf(int code)
124 | {
125 | for(RtspStatus status : RtspStatus.values()) {
126 | if(status.getCode() == code) return status;
127 | }
128 | throw new IllegalArgumentException("Non existent status code specified: " + code);
129 | }
130 |
131 | }
132 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/utils/SafeProxy.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 XpertSoftware
3 | *
4 | * Created By: cfloersch
5 | * Date: 4/17/2015
6 | */
7 | package xpertss.utils;
8 |
9 | import xpertss.threads.Threads;
10 |
11 | import java.lang.reflect.InvocationHandler;
12 | import java.lang.reflect.InvocationTargetException;
13 | import java.lang.reflect.Method;
14 | import java.lang.reflect.Proxy;
15 |
16 | /**
17 | * This class will wrap a given class and proxy calls to it catching any runtime
18 | * exceptions that may be thrown. Those exceptions will be sent to the calling
19 | * thread's uncaught exception handler and null will be returned.
20 | *
21 | * This is principally to be used with classes who's members do not return values
22 | * such as event listeners.
23 | *
24 | * @param
25 | */
26 | public class SafeProxy implements InvocationHandler {
27 |
28 | /**
29 | * Construct an instance of the SafeDispatcher compatible with the given listener class.
30 | */
31 | public static T newInstance(Class proxiedClass, T proxied)
32 | {
33 | return proxiedClass.cast(Proxy.newProxyInstance(proxiedClass.getClassLoader(),
34 | new Class[] { proxiedClass },
35 | new SafeProxy(proxied)));
36 | }
37 |
38 |
39 | private final T proxied;
40 |
41 |
42 | private SafeProxy(T proxied)
43 | {
44 | this.proxied = proxied;
45 | }
46 |
47 |
48 | @Override
49 | public Object invoke(Object proxy, final Method method, final Object[] args)
50 | throws Throwable
51 | {
52 | if ("equals".equals(method.getName())) {
53 | return (args[0] == proxy);
54 | } else if("hashCode".equals(method.getName())) {
55 | return hashCode();
56 | } else if("toString".equals(method.getName())) {
57 | return toString();
58 | } else {
59 | try {
60 | return method.invoke(proxied, args);
61 | } catch(InvocationTargetException e) {
62 | Threads.report(e.getTargetException());
63 | } catch(Throwable t) {
64 | Threads.report(t);
65 | }
66 | return null;
67 | }
68 | }
69 |
70 |
71 | @Override
72 | public String toString()
73 | {
74 | return "SafeProxy<" + proxied.toString() + ">";
75 | }
76 |
77 | }
78 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/utils/UserAgent.java:
--------------------------------------------------------------------------------
1 | package xpertss.utils;
2 |
3 | import xpertss.lang.Objects;
4 | import xpertss.lang.Strings;
5 | import xpertss.util.Config;
6 | import xpertss.util.Platform;
7 | import xpertss.util.Version;
8 |
9 | import java.util.BitSet;
10 |
11 | /**
12 | * The UserAgent class encapsulates the process creating a UserAgent string from
13 | * the underlying platform.
14 | *
15 | * A typical UserAgent string would look something like this:
16 | *
17 | * MMP/1.0 (Windows/6.1; amd64; Java/1.7) XpertRTSP/1.0
18 | *
19 | * In the above example the actual user agent application is identified by MMP/1.0.
20 | * The contents in the parenthesis are the OS name/version followed by the CPU
21 | * Architecture followed by the version of Java the client is running on. Finally,
22 | * the last portion represents this RTSP library and version.
23 | *
24 | * This class allows the user some control over the user agent sent to the server
25 | * with each request. For full control you may set the User-Agent header yourself
26 | * for each call or you may extend this class and override the {@link #toString}
27 | * method..
28 | */
29 | public class UserAgent {
30 |
31 | private final BitSet bitset = new BitSet(3);
32 |
33 | private String appName;
34 | private Version appVersion;
35 |
36 | /**
37 | * Accessible zero argument constructor for class wishing to extend and
38 | * override this implementation.
39 | */
40 | protected UserAgent() { }
41 |
42 | private UserAgent(String appName, Version appVersion)
43 | {
44 | this.appName = Strings.notEmpty(appName, "appName must be defined");
45 | this.appVersion = Objects.notNull(appVersion, "appVersion");
46 | }
47 |
48 |
49 | /**
50 | * Include the name of this RTSP library and its version in the user
51 | * agent header sent to the server with each request.
52 | */
53 | public void includeLibrary(boolean value)
54 | {
55 | bitset.set(0, !value);
56 | }
57 |
58 | /**
59 | * Returns {@code true} if the library portion is included in the user
60 | * agent string sent to the server with each request.
61 | */
62 | public boolean isLibraryIncluded()
63 | {
64 | return !bitset.get(0);
65 | }
66 |
67 |
68 |
69 | /**
70 | * Include the version of Java this library is running on in the user
71 | * agent header sent to the server with each request.
72 | */
73 | public void includeJava(boolean value)
74 | {
75 | bitset.set(1, !value);
76 | }
77 |
78 | /**
79 | * Returns {@code true} if the Java portion is included in the user
80 | * agent string sent to the server with each request.
81 | */
82 | public boolean isJavaIncluded()
83 | {
84 | return !bitset.get(1);
85 | }
86 |
87 |
88 |
89 | /**
90 | * Include the type of CPU this user agent is running on in the user
91 | * agent header sent to the server with each request.
92 | */
93 | public void includeCpu(boolean value)
94 | {
95 | bitset.set(2, !value);
96 | }
97 |
98 | /**
99 | * Returns {@code true} if the cpu portion is included in the user
100 | * agent string sent to the server with each request.
101 | */
102 | public boolean isCpuIncluded()
103 | {
104 | return !bitset.get(2);
105 | }
106 |
107 |
108 | @Override
109 | public String toString()
110 | {
111 | StringBuilder builder = new StringBuilder();
112 | builder.append(String.format("%s/%s", appName, appVersion));
113 |
114 | String osName = System.getProperty("os.name").split("\\s+")[0];
115 | Version osVersion = Platform.osVersion();
116 |
117 | builder.append(String.format(" (%s/%s", osName, osVersion));
118 | if(!bitset.get(2)) {
119 | String osArch = System.getProperty("os.arch");
120 | builder.append(String.format("; %s", osArch));
121 | }
122 | if(!bitset.get(1)) {
123 | Version vmVersion = Platform.javaVersion();
124 | builder.append(String.format("; Java/%s", vmVersion));
125 | }
126 | builder.append(")");
127 | if(!bitset.get(0)) {
128 | Config config = Config.load("version.properties", false);
129 | String libVersion = config.getProperty("version");
130 | builder.append(String.format(" XpertRTSP/%s", libVersion));
131 | }
132 |
133 | return builder.toString();
134 | }
135 |
136 |
137 | /**
138 | * Create a default UserAgent object with the specified application
139 | * name and version.
140 | *
141 | * @param appName The application name
142 | * @param appVersion The application version
143 | */
144 | public static UserAgent create(String appName, Version appVersion)
145 | {
146 | return new UserAgent(appName, appVersion);
147 | }
148 |
149 | }
150 |
--------------------------------------------------------------------------------
/src/main/java/xpertss/utils/Utils.java:
--------------------------------------------------------------------------------
1 | package xpertss.utils;
2 |
3 | import xpertss.lang.Integers;
4 | import xpertss.lang.Numbers;
5 | import xpertss.lang.Strings;
6 | import xpertss.mime.Header;
7 | import xpertss.mime.Headers;
8 | import xpertss.rtsp.RtspResponse;
9 |
10 | import java.net.InetSocketAddress;
11 | import java.net.SocketAddress;
12 | import java.net.URI;
13 | import java.net.URL;
14 | import java.nio.ByteBuffer;
15 | import java.nio.CharBuffer;
16 | import java.util.concurrent.TimeUnit;
17 |
18 | /**
19 | * Useful utilities
20 | */
21 | public class Utils {
22 |
23 | /**
24 | * Creates and returns an {@link InetSocketAddress} that represents the authority
25 | * of the given {@link java.net.URI}. If the {@link java.net.URI} does not define
26 | * a port then the specified default port is used instead.
27 | *
28 | * @throws NullPointerException if uri is {@code null}
29 | * @throws IllegalArgumentException if the port is outside the range 1 - 65545
30 | */
31 | public static SocketAddress createSocketAddress(URI uri, int defPort)
32 | {
33 | String authority = uri.getAuthority();
34 | if(Strings.isEmpty(authority))
35 | throw new IllegalArgumentException("uri does not define an authority");
36 | String[] parts = authority.split(":");
37 | int port = defPort;
38 | if(parts.length == 2) {
39 | port = Integers.parse(parts[1], defPort);
40 | Numbers.within(1, 65545, port, String.format("%d is an invalid port", port));
41 | }
42 | return new InetSocketAddress(parts[0], port);
43 | }
44 |
45 |
46 |
47 |
48 | public static T get(T[] items, int index)
49 | {
50 | if(items == null || index < 0) return null;
51 | return (items.length > index) ? items[index] : null;
52 | }
53 |
54 |
55 | public static String trimAndClear(StringBuilder buf)
56 | {
57 | return getAndClear(buf).trim();
58 | }
59 |
60 | public static String getAndClear(StringBuilder buf)
61 | {
62 | try {
63 | return buf.toString();
64 | } finally {
65 | buf.setLength(0);
66 | }
67 | }
68 |
69 |
70 | public static long computeTimeout(int millis)
71 | {
72 | if(millis == 0) millis = 60000; // default timeout of 60 seconds
73 | return System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(millis);
74 | }
75 |
76 |
77 |
78 |
79 |
80 |
81 | public static String consume(CharBuffer buf, boolean trim)
82 | {
83 | buf.flip();
84 | int offset = 0; int end = buf.limit();
85 | if(trim) {
86 | while(offset < end && buf.get(offset) == ' ') offset++;
87 | while(end > offset && buf.get(end - 1) == ' ') end--;
88 | }
89 | char[] result = new char[end-offset];
90 | buf.position(offset);
91 | buf.get(result).clear();
92 | return new String(result);
93 | }
94 |
95 |
96 | public static boolean isWhiteSpace(char c)
97 | {
98 | return (c == '\t' || c == '\r' || c == '\n' || c == ' ');
99 | }
100 |
101 |
102 | public static int maxIfZero(int value)
103 | {
104 | return (value == 0) ? Integer.MAX_VALUE : value;
105 | }
106 |
107 |
108 | public static String getHeader(RtspResponse response, String name)
109 | {
110 | return Headers.toString(response.getHeaders().getHeader(name));
111 | }
112 |
113 | public static String getBaseHeader(RtspResponse response, String name)
114 | {
115 | Header header = response.getHeaders().getHeader(name);
116 | return header.getValue(0).getValue();
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/services/xpertss.mime.spi.HeaderParserProvider:
--------------------------------------------------------------------------------
1 | xpertss.mime.impl.MailHeaderParserProvider
2 | xpertss.mime.impl.HttpHeaderParserProvider
3 | xpertss.mime.impl.RtspHeaderParserProvider
--------------------------------------------------------------------------------
/src/main/resources/version.properties:
--------------------------------------------------------------------------------
1 | version: ${project.version}
--------------------------------------------------------------------------------
/src/test/java/xpertss/mime/HeaderParserTest.java:
--------------------------------------------------------------------------------
1 | package xpertss.mime;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | public class HeaderParserTest {
8 |
9 |
10 | @Test
11 | public void testTransport()
12 | {
13 | Header header = HeaderParser.parse("Transport", "RTP/AVP/TCP;unicast;interleaved=0-1");
14 | assertNotNull(header.getValue(0));
15 | assertNotNull(header.getValue(0).getParameter("interleaved"));
16 | assertEquals("unicast", header.getValue(0).getParameter(0).getValue());
17 | }
18 |
19 | @Test
20 | public void testComplexWithEquals()
21 | {
22 | Header header = HeaderParser.parse("RTP-Info", "url=rtsp://stream.manheim.com:999/AVAIL.sdp/trackID=2,url=rtsp://stream.manheim.com:999/AVAIL.sdp/trackID=5");
23 | assertEquals(2, header.size());
24 |
25 | HeaderValue valueOne = header.getValue(0);
26 | assertEquals("rtsp://stream.manheim.com:999/AVAIL.sdp/trackID=2", valueOne.getValue());
27 |
28 | HeaderValue valueTwo = header.getValue(1);
29 | assertEquals("rtsp://stream.manheim.com:999/AVAIL.sdp/trackID=5", valueTwo.getValue());
30 |
31 | // TODO How would I access these HeaderValues by name when they both have the same name
32 | }
33 |
34 | @Test
35 | public void testSplit()
36 | {
37 | String[] parts = "url=rtsp://stream.manheim.com:999/AVAIL.sdp/trackID=2".split("=", 2);
38 | assertEquals(2, parts.length);
39 | }
40 |
41 | }
--------------------------------------------------------------------------------
/src/test/java/xpertss/mime/HeadersTest.java:
--------------------------------------------------------------------------------
1 | package xpertss.mime;
2 |
3 | import org.junit.Test;
4 |
5 | import java.util.Enumeration;
6 |
7 | import static org.junit.Assert.*;
8 |
9 | public class HeadersTest {
10 |
11 |
12 | @Test
13 | public void testEmpty()
14 | {
15 | Headers headers = new Headers(Headers.Type.Rtsp);
16 | assertNull(headers.getHeader("Content-Type"));
17 | assertFalse(headers.headers().hasMoreElements());
18 | }
19 |
20 | @Test
21 | public void testGetSet()
22 | {
23 | Headers headers = new Headers(Headers.Type.Rtsp);
24 | assertNull(headers.getHeader("Content-Length"));
25 | headers.setHeader("Content-Length", "200");
26 | assertNotNull(headers.getHeader("Content-Length"));
27 | assertEquals("200", Headers.toString(headers.getHeader("Content-Length")));
28 | assertTrue(headers.headers().hasMoreElements()); // should have an element
29 | assertTrue(headers.contains("Content-Length")); // should be found
30 | assertTrue(headers.contains("Content-length")); // case doesn't matter
31 | }
32 |
33 | @Test
34 | public void testOrdering()
35 | {
36 | Headers headers = new Headers(Headers.Type.Rtsp);
37 | headers.setHeader("Connection", "5");
38 | headers.setHeader("CSeq", "1");
39 | headers.setHeader("Content-Base", "3");
40 | headers.setHeader("Vary", "4");
41 | headers.setHeader("User-Agent", "2");
42 |
43 | Enumeration e = headers.headers();
44 | assertEquals("1", Headers.toString(e.nextElement()));
45 | assertEquals("2", Headers.toString(e.nextElement()));
46 | assertEquals("3", Headers.toString(e.nextElement()));
47 | assertEquals("4", Headers.toString(e.nextElement()));
48 | assertEquals("5", Headers.toString(e.nextElement()));
49 | }
50 |
51 | @Test
52 | public void testMultipleHeaders()
53 | {
54 | Headers headers = new Headers(Headers.Type.Rtsp);
55 | headers.addHeader("Received", "from localhost");
56 | headers.addHeader("Received", "from gateway");
57 |
58 | Enumeration e = headers.headers();
59 | assertEquals("from localhost", Headers.toString(e.nextElement()));
60 | assertEquals("from gateway", Headers.toString(e.nextElement()));
61 | assertFalse(e.hasMoreElements());
62 |
63 | assertEquals(2, headers.getHeaders("Received").length);
64 | assertEquals(2, headers.getHeaders("received").length);
65 |
66 | assertEquals(2, headers.remove("Received"));
67 | assertEquals(0, headers.remove("Received"));
68 | }
69 |
70 | @Test
71 | public void testSetIfNotSet()
72 | {
73 | Headers headers = new Headers(Headers.Type.Rtsp);
74 | headers.setIfNotSet("User-Agent", "one");
75 | headers.setIfNotSet("User-Agent", "two");
76 | assertEquals("one", Headers.toString(headers.getHeader("User-Agent")));
77 | assertEquals(1, headers.getHeaders("User-Agent").length);
78 |
79 | assertEquals(1, headers.remove("User-Agent"));
80 | }
81 |
82 | @Test
83 | public void testHeaderParameters()
84 | {
85 | Headers headers = new Headers(Headers.Type.Rtsp);
86 | headers.setIfNotSet("Session", "rzRIqDVnQVDRTppy;timeout=60");
87 |
88 | Header header = headers.getHeader("Session");
89 | assertEquals("rzRIqDVnQVDRTppy; timeout=60", Headers.toString(header));
90 | assertEquals("rzRIqDVnQVDRTppy", header.getValue(0).getValue());
91 | assertEquals("60", header.getValue(0).getParameter("timeout").getValue());
92 | }
93 |
94 |
95 | }
--------------------------------------------------------------------------------
/src/test/java/xpertss/rtsp/RtspClientTest.java:
--------------------------------------------------------------------------------
1 | package xpertss.rtsp;
2 |
3 | import org.junit.Test;
4 | import xpertss.lang.Range;
5 | import xpertss.media.MediaChannel;
6 | import xpertss.media.MediaConsumer;
7 | import xpertss.media.MediaType;
8 | import xpertss.sdp.MediaDescription;
9 | import xpertss.sdp.SessionDescription;
10 |
11 | import java.net.ConnectException;
12 | import java.net.URI;
13 | import java.net.UnknownHostException;
14 | import java.nio.ByteBuffer;
15 | import java.util.HashMap;
16 | import java.util.Map;
17 | import java.util.concurrent.TimeUnit;
18 | import java.util.function.Consumer;
19 |
20 |
21 | import static org.junit.Assert.*;
22 | import static org.mockito.Matchers.any;
23 | import static org.mockito.Matchers.same;
24 | import static org.mockito.Mockito.mock;
25 | import static org.mockito.Mockito.times;
26 | import static org.mockito.Mockito.verify;
27 | import static xpertss.nio.ReadyState.Closed;
28 | import static xpertss.nio.ReadyState.Closing;
29 | import static xpertss.nio.ReadyState.Connected;
30 | import static xpertss.nio.ReadyState.Connecting;
31 | import static xpertss.nio.ReadyState.Open;
32 |
33 | public class RtspClientTest {
34 |
35 |
36 | @Test
37 | public void testClientConnectSequence()
38 | {
39 | RtspHandler handler = mock(RtspHandler.class);
40 | RtspClient client = new RtspClient();
41 | RtspSession session = client.open(handler, URI.create("rtsp://stream.manheim.com:999/AVAIL.sdp"));
42 |
43 | assertEquals(Open, session.getReadyState());
44 | session.connect(500);
45 | assertEquals(Connecting, session.getReadyState());
46 |
47 | while(session.getReadyState() == Connecting);
48 |
49 | verify(handler, times(1)).onConnect(same(session));
50 | assertEquals(Connected, session.getReadyState());
51 |
52 | session.close();
53 | assertEquals(Closing, session.getReadyState());
54 |
55 | while(session.getReadyState() == Closing);
56 | assertEquals(Closed, session.getReadyState());
57 |
58 | verify(handler, times(1)).onClose(same(session));
59 | }
60 |
61 |
62 | @Test
63 | public void testClientConnectSequenceTimeout()
64 | {
65 | RtspHandler handler = mock(RtspHandler.class);
66 | RtspClient client = new RtspClient();
67 | RtspSession session = client.open(handler, URI.create("rtsp://10.0.0.1:999/AVAIL.sdp"));
68 |
69 | assertEquals(Open, session.getReadyState());
70 | session.connect(100);
71 | assertEquals(Connecting, session.getReadyState());
72 |
73 | while(session.getReadyState() == Connecting);
74 |
75 | verify(handler, times(1)).onFailure(same(session), any(ConnectException.class));
76 | assertEquals(Closed, session.getReadyState());
77 | }
78 |
79 | @Test
80 | public void testClientConnectSequenceUnknownHost()
81 | {
82 | RtspHandler handler = mock(RtspHandler.class);
83 | RtspClient client = new RtspClient();
84 | RtspSession session = client.open(handler, URI.create("rtsp://doesnot.exist.host.com:999/AVAIL.sdp"));
85 |
86 | assertEquals(Open, session.getReadyState());
87 | session.connect(100);
88 | assertEquals(Connecting, session.getReadyState());
89 |
90 | while(session.getReadyState() == Connecting);
91 |
92 | verify(handler, times(1)).onFailure(same(session), any(UnknownHostException.class));
93 | assertEquals(Closed, session.getReadyState());
94 | }
95 |
96 |
97 |
98 |
99 |
100 | @Test
101 | public void testRtspPlayer()
102 | {
103 | RtspClient client = new RtspClient();
104 | RtspPlayer player = new RtspPlayer(client, new MediaConsumer() {
105 |
106 | private Map> channels = new HashMap<>();
107 |
108 | @Override
109 | public MediaDescription[] select(SessionDescription sdp)
110 | {
111 | return sdp.getMediaDescriptions();
112 | }
113 |
114 | @Override
115 | public void createChannel(MediaChannel channel)
116 | {
117 | final MediaType type = channel.getType();
118 | Range range = channel.getChannels();
119 | channels.put(range.getLower(), new Consumer() {
120 | long start = System.nanoTime();
121 | @Override public void accept(ByteBuffer data) {
122 | long millis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
123 | int seq = (int) (data.getShort(2) & 0xffff);
124 | long ts = (long) (data.getInt(4) & 0xffffffff);
125 | System.out.println(String.format("%s: %010d {seq=%05d, ts=%d, size=%04d}", type.name(), millis, seq, ts, data.remaining()));
126 | }
127 | });
128 | channels.put(range.getUpper(), new Consumer() {
129 | @Override
130 | public void accept(ByteBuffer byteBuffer) {
131 | System.out.println(String.format("%s - RTP Sender Report Received", type.name()));
132 | }
133 | });
134 | }
135 |
136 | @Override
137 | public void destroyChannels()
138 | {
139 | System.out.println("destroy called");
140 | channels.clear();
141 | }
142 |
143 | @Override
144 | public void consume(int channelId, ByteBuffer data)
145 | {
146 | Consumer handler = channels.get(channelId);
147 | if(handler == null) System.out.println(String.format("Received packet on unknown channel %d", channelId));
148 | else handler.accept(data);
149 | }
150 |
151 | @Override
152 | public void handle(Throwable t)
153 | {
154 | t.printStackTrace();
155 | }
156 |
157 |
158 | });
159 |
160 | player.setReadTimeout(5000);
161 | player.start(URI.create("rtsp://stream.manheim.com:999/AVAIL.sdp"));
162 | assertEquals(RtspState.Activating, player.getState());
163 | client.await(3, TimeUnit.SECONDS);
164 | assertEquals(RtspState.Active, player.getState());
165 | player.pause();
166 | assertEquals(RtspState.Pausing, player.getState());
167 | client.await(3, TimeUnit.SECONDS);
168 | assertEquals(RtspState.Paused, player.getState());
169 | player.play();
170 | assertEquals(RtspState.Activating, player.getState());
171 | client.await(3, TimeUnit.SECONDS);
172 | assertEquals(RtspState.Active, player.getState());
173 | player.stop();
174 | assertEquals(RtspState.Stopping, player.getState());
175 | client.await();
176 | assertEquals(RtspState.Stopped, player.getState());
177 | }
178 |
179 |
180 |
181 | }
--------------------------------------------------------------------------------
/src/test/java/xpertss/rtsp/RtspMethodTest.java:
--------------------------------------------------------------------------------
1 | package xpertss.rtsp;
2 |
3 | import org.junit.Test;
4 | import xpertss.lang.Booleans;
5 |
6 | import java.util.concurrent.ConcurrentHashMap;
7 | import java.util.concurrent.ConcurrentMap;
8 |
9 | import static org.junit.Assert.*;
10 |
11 | public class RtspMethodTest {
12 |
13 |
14 |
15 | @Test
16 | public void testBooleansDefaultUnboxedValue()
17 | {
18 | ConcurrentMap map = new ConcurrentHashMap<>();
19 | assertFalse(Booleans.isTrue(map.get("NonExistent")));
20 | assertFalse(Booleans.isTrue(map.remove("NonExistent")));
21 | assertFalse(map.replace("NonExistent", false, true));
22 | //assertFalse(map.replace(null, false, true));
23 | }
24 |
25 | }
--------------------------------------------------------------------------------
/src/test/java/xpertss/rtsp/RtspResponseTest.java:
--------------------------------------------------------------------------------
1 | package xpertss.rtsp;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | public class RtspResponseTest {
8 |
9 | @Test
10 | public void testResponseSplit()
11 | {
12 | String response = "500 Internal Error";
13 | String[] parts = response.split("\\s+", 2);
14 | assertEquals(2, parts.length);
15 | assertEquals("500", parts[0]);
16 | assertEquals("Internal Error", parts[1]);
17 | }
18 |
19 |
20 | }
--------------------------------------------------------------------------------
/src/test/java/xpertss/utils/UserAgentTest.java:
--------------------------------------------------------------------------------
1 | package xpertss.utils;
2 |
3 | import org.junit.Test;
4 | import xpertss.util.Version;
5 |
6 | import static org.junit.Assert.*;
7 |
8 | public class UserAgentTest {
9 |
10 | @Test
11 | public void testUserAgent()
12 | {
13 | String userAgent = UserAgent.create("MMP", new Version(2,1)).toString();
14 | assertTrue(userAgent.startsWith("MMP/2.1"));
15 | assertTrue(userAgent.contains("XpertRTSP/"));
16 | assertTrue(userAgent.contains(System.getProperty("os.arch")));
17 | assertTrue(userAgent.contains("Java"));
18 | }
19 |
20 | @Test
21 | public void testUserAgentNoCpu()
22 | {
23 | UserAgent agent = UserAgent.create("MMP", new Version(2,1));
24 | agent.includeCpu(false);
25 | String userAgent = agent.toString();
26 | assertTrue(userAgent.startsWith("MMP/2.1"));
27 | assertTrue(userAgent.contains("XpertRTSP/"));
28 | assertFalse(userAgent.contains(System.getProperty("os.arch")));
29 | assertTrue(userAgent.contains("Java"));
30 | }
31 |
32 | @Test
33 | public void testUserAgentNoJava()
34 | {
35 | UserAgent agent = UserAgent.create("MMP", new Version(2,1));
36 | agent.includeJava(false);
37 | String userAgent = agent.toString();
38 | assertTrue(userAgent.startsWith("MMP/2.1"));
39 | assertTrue(userAgent.contains("XpertRTSP/"));
40 | assertTrue(userAgent.contains(System.getProperty("os.arch")));
41 | assertFalse(userAgent.contains("Java"));
42 | }
43 |
44 | @Test
45 | public void testUserAgentNoLibrary()
46 | {
47 | UserAgent agent = UserAgent.create("MMP", new Version(2,1));
48 | agent.includeLibrary(false);
49 | String userAgent = agent.toString();
50 | assertTrue(userAgent.startsWith("MMP/2.1"));
51 | assertFalse(userAgent.contains("XpertRTSP/"));
52 | assertTrue(userAgent.contains(System.getProperty("os.arch")));
53 | assertTrue(userAgent.contains("Java"));
54 | }
55 |
56 | }
--------------------------------------------------------------------------------
/src/test/java/xpertss/utils/UtilsTest.java:
--------------------------------------------------------------------------------
1 | package xpertss.utils;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | public class UtilsTest {
8 |
9 |
10 |
11 |
12 |
13 |
14 | }
--------------------------------------------------------------------------------