
xpertss.sdp.SessionParser Maven / Gradle / Ivy
/**
* Created By: cfloersch
* Date: 6/2/13
* Copyright 2013 XpertSoftware
*/
package xpertss.sdp;
import xpertss.lang.Integers;
import xpertss.lang.Longs;
import xpertss.lang.Objects;
import xpertss.lang.Strings;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
/**
* Parser to parse SessionDescription objects from standard SDP files.
*
* This parser performs only rudimentary validation of data. Numbers
* must properly parse to numbers, the minimum number of fields must
* be defined, and field ordering is enforced. However, relational
* rules are not currently implemented.
*
* Session description
* v= (one protocol version)
* o= (one originator and session identifier)
* s= (one session name)
* i= (zero or one session information)
* u= (zero or one URI of description)
* e= (zero or more email address)
* p= (zero or more phone number)
* c= (zero or one connection information)
* b= (zero or more bandwidth information lines)
* One or more time descriptions ("t=" and "r=" lines; see below)
* z= (zero or one time zone adjustments)
* k= (zero or one encryption key)
* a= (zero or more session attribute lines)
* Zero or more media descriptions
*
* Time description
* t= (one time the session is active)
* r= (zero or more repeat times)
*
* Media description, if present
* m= (one media name and transport address)
* i= (zero or one media title)
* c= (zero or one connection information)
* b= (zero or more bandwidth information lines)
* k= (zero or one encryption key)
* a= (zero or more media attribute lines)
*
*/
public class SessionParser {
private static final Set validchars = new HashSet<>();
static {
validchars.add("v");
validchars.add("o");
validchars.add("s");
validchars.add("i");
validchars.add("u");
validchars.add("e");
validchars.add("p");
validchars.add("c");
validchars.add("b");
validchars.add("t");
validchars.add("r");
validchars.add("z");
validchars.add("k");
validchars.add("a");
validchars.add("m");
}
/**
* Parse a string which represents an SDP file returning a Session Description if
* it successfully parsed the data.
*
* @throws SdpParseException If an error occurs parsing the structure of the document
* @throws NullPointerException If the supplied source string is null
*/
public SessionDescription parse(String str) throws SdpParseException, NullPointerException
{
return parse(new Scanner(str));
}
/**
* Parse the contents of a given sdp file identified by a Path object returning a Session
* Description if it successfully parsed the data.
*
* @throws SdpParseException If an error occurs parsing the structure of the document
* @throws NullPointerException If the supplied source path is null
* @throws IOException If an I/O error occurs opening source
*/
public SessionDescription parse(Path path) throws SdpParseException, NullPointerException, IOException
{
return parse(new Scanner(path, "UTF-8"));
}
/**
* Parse the sdp contents from a given Reader object returning a Session Description if it
* successfully parsed the data.
*
* @throws SdpParseException If an error occurs parsing the structure of the document
* @throws NullPointerException If the supplied source reader is null
*/
public SessionDescription parse(Reader reader) throws SdpParseException, NullPointerException
{
return parse(new Scanner(reader));
}
/**
* Parse the sdp contents from a given InputStream object returning a Session Description if it
* successfully parsed the data.
*
* @throws SdpParseException If an error occurs parsing the structure of the document
* @throws NullPointerException If the supplied source stream is null
*/
public SessionDescription parse(InputStream stream) throws SdpParseException, NullPointerException
{
return parse(new Scanner(stream, "UTF-8"));
}
private SessionDescription parse(Scanner scanner) throws SdpParseException
{
try {
SessionBuilder builder = SessionBuilder.create();
SessionFieldParser chain = createParserChain();
while(scanner.hasNextLine()) {
String line = scanner.nextLine();
if(Strings.isEmpty(line)) break;
if(line.length() < 3 || line.charAt(1) != '=') throw new SdpParseException("invalid line format: " + line);
if(!validchars.contains(Character.toString(line.charAt(0))))
throw new SdpParseException("invalid field: " + line);
chain = chain.parse(builder, line);
}
chain.finish(builder);
return builder.build();
} finally {
scanner.close();
}
}
private SessionFieldParser createParserChain()
{
SessionFieldParser parser = new MediaFieldParser();
parser = new KeyFieldParser(new AttributeFieldParser(parser));
parser = new TimeFieldParser(new TimeZonesFieldParser(parser));
parser = new ConnectionFieldParser(new BandwidthFieldParser(parser));
parser = new EmailFieldParser(new PhoneFieldParser(parser));
parser = new InfoFieldParser(new UriFieldParser(parser));
parser = new OriginFieldParser(new SessionNameFieldParser(parser));
return new VersionFieldParser((parser));
}
private interface SessionFieldParser {
public SessionFieldParser parse(SessionBuilder builder, String line);
public void finish(SessionBuilder builder);
}
private interface MediaSubFieldParser {
public MediaSubFieldParser parse(MediaBuilder builder, String line);
public void finish(MediaBuilder builder);
}
private class VersionFieldParser implements SessionFieldParser {
private SessionFieldParser next;
public VersionFieldParser(SessionFieldParser next)
{
this.next = Objects.notNull(next, "next may not be null");
}
@Override
public SessionFieldParser parse(SessionBuilder builder, String line)
{
if(line.startsWith("v")) {
int version = Integers.parse(line.substring(2), -1);
if(version < 0) throw new SdpParseException("invalid version specified: " + line.substring(2));
builder.setVersion(version);
return next;
}
throw new SdpParseException("invalid session description: expecting version");
}
@Override
public void finish(SessionBuilder builder)
{
throw new SdpParseException("premature end of stream: expecting version");
}
}
private class OriginFieldParser implements SessionFieldParser {
private SessionFieldParser next;
public OriginFieldParser(SessionFieldParser next)
{
this.next = Objects.notNull(next, "next may not be null");
}
@Override
public SessionFieldParser parse(SessionBuilder builder, String line)
{
if(line.startsWith("o")) {
String[] parts = line.substring(2).split("\\s+");
if(parts.length != 6) throw new SdpParseException("invalid origin line: " + line.substring(2));
OriginBuilder origin = OriginBuilder.create().setUsername(parts[0]);
long sessionId = Longs.parse(parts[1], -1);
if(sessionId < 0) throw new SdpParseException("invalid origin sessionId: " + parts[1]);
origin.setSessionId(sessionId);
long version = Longs.parse(parts[2], -1);
if(version < 0) throw new SdpParseException("invalid origin session version: " + parts[2]);
origin.setSessionVersion(version);
origin.setNetworkType(parts[3]).setAddressType(parts[4]).setAddress(parts[5]);
builder.setOrigin(origin.build());
return next;
}
throw new SdpParseException("invalid session description: expecting origin");
}
@Override
public void finish(SessionBuilder builder)
{
throw new SdpParseException("premature end of stream: expecting origin");
}
}
private class SessionNameFieldParser implements SessionFieldParser {
private SessionFieldParser next;
public SessionNameFieldParser(SessionFieldParser next)
{
this.next = Objects.notNull(next, "next may not be null");
}
@Override
public SessionFieldParser parse(SessionBuilder builder, String line)
{
if(line.startsWith("s")) {
builder.setSessionName(line.substring(2));
return next;
}
throw new SdpParseException("invalid session description: expecting session name");
}
@Override
public void finish(SessionBuilder builder)
{
throw new SdpParseException("premature end of stream: expecting session name");
}
}
private class InfoFieldParser implements SessionFieldParser {
private SessionFieldParser next;
public InfoFieldParser(SessionFieldParser next)
{
this.next = Objects.notNull(next, "next may not be null");
}
@Override
public SessionFieldParser parse(SessionBuilder builder, String line)
{
if(line.startsWith("i")) {
builder.setInfo(line.substring(2));
return next;
}
return next.parse(builder, line);
}
@Override
public void finish(SessionBuilder builder)
{
next.finish(builder);
}
}
private class UriFieldParser implements SessionFieldParser {
private SessionFieldParser next;
public UriFieldParser(SessionFieldParser next)
{
this.next = Objects.notNull(next, "next may not be null");
}
@Override
public SessionFieldParser parse(SessionBuilder builder, String line)
{
if(line.startsWith("u")) {
builder.setUri(line.substring(2));
return next;
}
return next.parse(builder, line);
}
@Override
public void finish(SessionBuilder builder)
{
next.finish(builder);
}
}
private class EmailFieldParser implements SessionFieldParser {
private SessionFieldParser next;
public EmailFieldParser(SessionFieldParser next)
{
this.next = Objects.notNull(next, "next may not be null");
}
@Override
public SessionFieldParser parse(SessionBuilder builder, String line)
{
if(line.startsWith("e")) {
builder.addEmail(line.substring(2));
return this;
}
return next.parse(builder, line);
}
@Override
public void finish(SessionBuilder builder)
{
next.finish(builder);
}
}
private class PhoneFieldParser implements SessionFieldParser {
private SessionFieldParser next;
public PhoneFieldParser(SessionFieldParser next)
{
this.next = Objects.notNull(next, "next may not be null");
}
@Override
public SessionFieldParser parse(SessionBuilder builder, String line)
{
if(line.startsWith("p")) {
builder.addPhone(line.substring(2));
return this;
}
return next.parse(builder, line);
}
@Override
public void finish(SessionBuilder builder)
{
next.finish(builder);
}
}
private class ConnectionFieldParser implements SessionFieldParser {
private SessionFieldParser next;
public ConnectionFieldParser(SessionFieldParser next)
{
this.next = Objects.notNull(next, "next may not be null");
}
@Override
public SessionFieldParser parse(SessionBuilder builder, String line)
{
if(line.startsWith("c")) {
String[] parts = line.substring(2).split("\\s+");
if(parts.length != 3) throw new SdpParseException("invalid connection line: " + line.substring(2));
builder.setConnection(parts[2], parts[1], parts[0]);
return next;
}
return next.parse(builder, line);
}
@Override
public void finish(SessionBuilder builder)
{
next.finish(builder);
}
}
private class BandwidthFieldParser implements SessionFieldParser {
private SessionFieldParser next;
public BandwidthFieldParser(SessionFieldParser next)
{
this.next = Objects.notNull(next, "next may not be null");
}
@Override
public SessionFieldParser parse(SessionBuilder builder, String line)
{
if(line.startsWith("b")) {
String[] parts = line.substring(2).split(":");
if(parts.length != 2) throw new SdpParseException("invalid bandwidth line: " + line.substring(2));
int kbps = Integers.parse(parts[1], -1);
if(kbps < 0) throw new SdpParseException("invalid bandwidth value specified: " + parts[1]);
builder.addBandwidth(parts[0], kbps);
return this;
}
return next.parse(builder, line);
}
@Override
public void finish(SessionBuilder builder)
{
next.finish(builder);
}
}
private class TimeFieldParser implements SessionFieldParser {
private SessionFieldParser next;
private TimeBuilder time;
public TimeFieldParser(SessionFieldParser next)
{
this.next = Objects.notNull(next, "next may not be null");
}
@Override
public SessionFieldParser parse(SessionBuilder builder, String line)
{
if(line.startsWith("t")) {
if(time != null) builder.addTimeDescription(time.build());
String[] parts = line.substring(2).split("\\s+");
if(parts.length != 2) throw new SdpParseException("invalid time field: " + line.substring(2));
long start = Longs.parse(parts[0], -1);
if(start < 0) throw new SdpParseException("invalid start time: " + parts[0]);
long stop = Longs.parse(parts[1], -1);
if(stop < 0) throw new SdpParseException("invalid stop time: " + parts[1]);
time = TimeBuilder.create().setTime(Utils.toDate(start), Utils.toDate(stop));
return this;
} else if(line.startsWith("r")) {
if(time == null) throw new SdpParseException("invalid session description: expecting time");
String[] parts = line.substring(2).split("\\s+");
if(parts.length < 3) throw new SdpParseException("invalid repeat time field: " + line.substring(2));
long interval = fromCompactTime(parts[0]);
if(interval < 1) throw new SdpParseException("invalid repeat time interval: " + parts[0]);
long duration = fromCompactTime(parts[1]);
if(duration < 1) throw new SdpParseException("invalid repeat time duration: " + parts[1]);
long[] offsets = new long[parts.length - 2];
for(int i = 0; i < offsets.length; i++) {
offsets[i] = fromCompactTime(parts[i+2]);
if(offsets[i] < 0) throw new SdpParseException("invalid repeat time offset: " + parts[i+2]);
}
time.addRepeatTime(interval, duration, offsets);
return this;
} else if(time != null) {
builder.addTimeDescription(time.build());
return next.parse(builder, line);
}
throw new SdpParseException("invalid session description: expecting time");
}
@Override
public void finish(SessionBuilder builder)
{
if(time != null) builder.addTimeDescription(time.build());
else throw new SdpParseException("premature end of stream: expecting time");
}
}
private class TimeZonesFieldParser implements SessionFieldParser {
private SessionFieldParser next;
public TimeZonesFieldParser(SessionFieldParser next)
{
this.next = Objects.notNull(next, "next may not be null");
}
@Override
public SessionFieldParser parse(SessionBuilder builder, String line)
{
if(line.startsWith("z")) {
String[] parts = line.substring(2).split("\\s+");
if(parts.length % 2 != 0) throw new SdpParseException("invalid timezones field: " + line.substring(2));
for(int i = 0; i < parts.length - 1; i += 2) {
long date = Longs.parse(parts[i], -1);
if(date < 0) throw new SdpParseException("invalid date found: " + parts[i]);
builder.addTimeAdjustment(Utils.toDate(date), fromCompactTime(parts[i + 1]));
}
return next;
}
return next.parse(builder, line);
}
@Override
public void finish(SessionBuilder builder)
{
next.finish(builder);
}
}
private class KeyFieldParser implements SessionFieldParser {
private SessionFieldParser next;
public KeyFieldParser(SessionFieldParser next)
{
this.next = Objects.notNull(next, "next may not be null");
}
@Override
public SessionFieldParser parse(SessionBuilder builder, String line)
{
if(line.startsWith("k")) {
int idx = line.indexOf(":");
if(idx < 0) {
builder.setKey(line.substring(2), null);
} else if(idx < line.length() - 1) {
builder.setKey(line.substring(2, idx), line.substring(idx + 1));
} else {
builder.setKey(line.substring(2, idx), null);
}
return next;
}
return next.parse(builder, line);
}
@Override
public void finish(SessionBuilder builder)
{
next.finish(builder);
}
}
private class AttributeFieldParser implements SessionFieldParser {
private SessionFieldParser next;
public AttributeFieldParser(SessionFieldParser next)
{
this.next = Objects.notNull(next, "next may not be null");
}
@Override
public SessionFieldParser parse(SessionBuilder builder, String line)
{
if(line.startsWith("a")) {
int idx = line.indexOf(":");
if(idx < 0) {
builder.addAttribute(line.substring(2), null);
} else if(idx == line.length() - 1) {
builder.addAttribute(line.substring(2, idx), null);
} else {
builder.addAttribute(line.substring(2, idx), line.substring(idx+1));
}
return this;
}
return next.parse(builder, line);
}
@Override
public void finish(SessionBuilder builder)
{
next.finish(builder);
}
}
private class MediaFieldParser implements SessionFieldParser {
private MediaBuilder media;
private MediaSubFieldParser chain;
@Override
public SessionFieldParser parse(SessionBuilder builder, String line)
{
if(line.startsWith("m")) {
if(media != null) {
builder.addMediaDescription(media.build());
}
media = MediaBuilder.create();
chain = createParserChain();
String[] parts = line.substring(2).split("\\s+");
if(parts.length < 4) throw new SdpParseException("incomplete media field: " + line.substring(2));
String[] ports = parts[1].split("/", 2);
int port = Integers.parse(ports[0], -1);
if(port < 0 || port > 65535) throw new SdpParseException("found invalid port: " + ports[0]);
int count = (ports.length < 2) ? 1 : Integers.parse(ports[1], -1);
if(count < 1) throw new SdpParseException("found invalid port count: " + ports[1]);
int[] formats = new int[parts.length - 3];
for(int i = 0; i < formats.length; i++) {
formats[i] = Integers.parse(parts[i+3], -1);
if(formats[i] < 0) throw new SdpParseException("invalid format found: " + parts[i+3]);
}
media.setMedia(parts[0], port, count, parts[2], formats);
} else if(media != null) {
chain = chain.parse(media, line);
} else {
throw new SdpParseException("invalid session description: expecting media");
}
return this;
}
@Override
public void finish(SessionBuilder builder)
{
if(media != null) {
chain.finish(media);
builder.addMediaDescription(media.build());
}
}
private MediaSubFieldParser createParserChain()
{
MediaSubFieldParser parser = new AttributeMediaSubFieldParser();
parser = new KeyMediaSubFieldParser(parser);
parser = new BandwidthMediaSubFieldParser(parser);
parser = new ConnectionMediaSubFieldParser(parser);
return new InfoMediaSubFieldParser(parser);
}
private class InfoMediaSubFieldParser implements MediaSubFieldParser {
private MediaSubFieldParser next;
public InfoMediaSubFieldParser(MediaSubFieldParser next)
{
this.next = Objects.notNull(next, "next may not be null");
}
@Override
public MediaSubFieldParser parse(MediaBuilder builder, String line)
{
if(line.startsWith("i")) {
builder.setInfo(line.substring(2));
return next;
}
return next.parse(builder, line);
}
@Override
public void finish(MediaBuilder builder)
{
next.finish(builder);
}
}
private class ConnectionMediaSubFieldParser implements MediaSubFieldParser {
private MediaSubFieldParser next;
public ConnectionMediaSubFieldParser(MediaSubFieldParser next)
{
this.next = Objects.notNull(next, "next may not be null");
}
@Override
public MediaSubFieldParser parse(MediaBuilder builder, String line)
{
if(line.startsWith("c")) {
String[] parts = line.substring(2).split("\\s+");
if(parts.length != 3) throw new SdpParseException("invalid connection line: " + line.substring(2));
builder.setConnection(parts[2], parts[1], parts[0]);
return next;
}
return next.parse(builder, line);
}
@Override
public void finish(MediaBuilder builder)
{
next.finish(builder);
}
}
private class BandwidthMediaSubFieldParser implements MediaSubFieldParser {
private MediaSubFieldParser next;
public BandwidthMediaSubFieldParser(MediaSubFieldParser next)
{
this.next = Objects.notNull(next, "next may not be null");
}
@Override
public MediaSubFieldParser parse(MediaBuilder builder, String line)
{
if(line.startsWith("b")) {
String[] parts = line.substring(2).split(":");
if(parts.length != 2) throw new SdpParseException("invalid bandwidth line: " + line.substring(2));
int kbps = Integers.parse(parts[1], -1);
if(kbps < 0) throw new SdpParseException("invalid bandwidth value specified: " + parts[1]);
builder.addBandwidth(parts[0], kbps);
return this;
}
return next.parse(builder, line);
}
@Override
public void finish(MediaBuilder builder)
{
next.finish(builder);
}
}
private class KeyMediaSubFieldParser implements MediaSubFieldParser {
private MediaSubFieldParser next;
public KeyMediaSubFieldParser(MediaSubFieldParser next)
{
this.next = Objects.notNull(next, "next may not be null");
}
@Override
public MediaSubFieldParser parse(MediaBuilder builder, String line)
{
if(line.startsWith("k")) {
int idx = line.indexOf(":");
if(idx < 0) {
builder.setKey(line.substring(2), null);
} else if(idx < line.length() - 1) {
builder.setKey(line.substring(2, idx), line.substring(idx + 1));
} else {
builder.setKey(line.substring(2, idx), null);
}
return next;
}
return next.parse(builder, line);
}
@Override
public void finish(MediaBuilder builder)
{
next.finish(builder);
}
}
private class AttributeMediaSubFieldParser implements MediaSubFieldParser {
@Override
public MediaSubFieldParser parse(MediaBuilder builder, String line)
{
if(line.startsWith("a")) {
int idx = line.indexOf(":");
if(idx < 0) {
builder.addAttribute(line.substring(2), null);
} else if(idx == line.length() - 1) {
builder.addAttribute(line.substring(2, idx), null);
} else {
builder.addAttribute(line.substring(2, idx), line.substring(idx+1));
}
return this;
}
throw new SdpParseException("misplaced field: " + line);
}
@Override
public void finish(MediaBuilder builder)
{
// what to do??
}
}
}
private static long fromCompactTime(String compact)
{
try {
char lastChar = compact.charAt(compact.length() - 1);
if(!Character.isDigit(lastChar)) {
long value = Long.parseLong(compact.substring(0, compact.length() - 1));
if(lastChar == 'd') {
return DAYS.toSeconds(value);
} else if(lastChar == 'h') {
return HOURS.toSeconds(value);
} else if(lastChar == 'm') {
return MINUTES.toSeconds(value);
} else if(lastChar == 's') {
return SECONDS.toSeconds(value);
}
throw new SdpParseException("unknown time unit found: " + compact);
} else {
return Long.parseLong(compact);
}
} catch(NumberFormatException nfe) {
throw new SdpParseException("invalid time unit found: " + compact);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy