/*
* Digital Audio Access Protocol (DAAP) Library
* Copyright (C) 2004-2010 Roger Kapsi
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.ardverk.daap;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import javax.print.URIException;
import org.apache.http.Header;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A DAAP request. The data of this class is submitted by the client (e.g.
* iTunes) and the data is used to create a appropriate DaapResponse.
*
* @author Roger Kapsi
*/
public class DaapRequest {
public static final String AUTHORIZATION = "Authorization";
public static final String CLIENT_DAAP_VERSION = "Client-DAAP-Version";
public static final String USER_AGENT = "User-Agent";
/** "/server-info" */
public static final int SERVER_INFO = 1;
/** "/content-codes" */
public static final int CONTENT_CODES = 2;
/** "/login" */
public static final int LOGIN = 3;
/** "/logout" */
public static final int LOGOUT = 4;
/** "/update" */
public static final int UPDATE = 5;
/** "/resolve" */
public static final int RESOLVE = 6;
/** "/databases" */
public static final int DATABASES = 7;
/** "/databases/databaseId/items" */
public static final int DATABASE_SONGS = 8;
/** "/databases/databaseId/containers" */
public static final int DATABASE_PLAYLISTS = 9;
/** "/databases/databaseId/containers/containerId/items" */
public static final int PLAYLIST_SONGS = 10;
/** "/databases/databaseId/items/itemId.format" */
public static final int SONG = 11;
private static final Logger LOG = LoggerFactory
.getLogger(DaapRequest.class);
private URI uri;
private Map<String, String> queryMap;
private SessionId sessionId = SessionId.INVALID;
private int revisionNumber = DaapUtil.NULL;
private int delta = DaapUtil.NULL;
private List<String> meta;
private String metaString;
private int requestType = DaapUtil.NULL;
private long databaseId = DaapUtil.NULL;
private long containerId = DaapUtil.NULL;
private long itemId = DaapUtil.NULL;
private List<Header> headers;
private boolean isServerSideRequest;
private boolean isUpdateType;
private DaapConnection connection;
/**
* Create a new DaapRequest
*/
private DaapRequest(DaapConnection connection) {
this.connection = connection;
headers = new ArrayList<Header>();
}
/**
* Creates a server side fake update DaapRequest to issue an update
*
* @param sessionId
* @param revisionNumber
* @param delta
*/
public DaapRequest(DaapConnection connection, SessionId sessionId,
int revisionNumber, int delta) {
this(connection);
this.sessionId = sessionId;
this.revisionNumber = revisionNumber;
this.delta = delta;
this.requestType = UPDATE;
this.isServerSideRequest = true;
this.isUpdateType = false;
}
/**
* Creates a DaapRequest from the the requestLine
*
* @param requestLine
* @throw URIException
*/
public DaapRequest(DaapConnection connection, String requestLine)
throws URISyntaxException {
this(connection);
URI uri = null;
try {
StringTokenizer st = new StringTokenizer(requestLine, " ");
st.nextToken(); // method
try {
uri = new URI(st.nextToken());
} catch (URISyntaxException err) {
LOG.error("URISyntaxException", err);
}
st.nextToken(); // protocol
} catch (NoSuchElementException err) {
LOG.error("NoSuchElementException", err);
}
this.isServerSideRequest = false;
this.isUpdateType = false;
setURI(uri);
}
/**
* Creates a new DaapRequest
*
* @param method
* @param uri
* @param protocol
* @throw URIException
*/
public DaapRequest(DaapConnection connection, String method, URI uri,
String protocol) throws URISyntaxException {
this(connection);
this.isServerSideRequest = false;
this.isUpdateType = false;
setURI(uri);
}
/**
* Sets and parses the URI. Note: if URIException is thrown then is this
* Request in an inconsistent state!
*
* @param uri
* @throws URIException
*/
private void setURI(URI uri) throws URISyntaxException {
this.uri = uri;
if (uri != null) {
String path = uri.getPath();
this.queryMap = DaapUtil.parseQuery(uri.getQuery());
if (path.equals("/server-info")) {
requestType = SERVER_INFO;
} else if (path.equals("/content-codes")) {
requestType = CONTENT_CODES;
} else if (path.equals("/login")) {
requestType = LOGIN;
} else if (path.equals("/logout")) {
requestType = LOGOUT;
} else if (path.equals("/update")) {
requestType = UPDATE;
} else if (path.equals("/resolve")) {
requestType = RESOLVE;
}
if (queryMap.containsKey("session-id")) {
sessionId = SessionId
.parseSessionId(queryMap.get("session-id"));
}
if (!SessionId.INVALID.equals(sessionId)) {
if (queryMap.containsKey("revision-number")) {
revisionNumber = Integer.parseInt(queryMap
.get("revision-number"));
}
if (queryMap.containsKey("delta")) {
delta = Integer.parseInt(queryMap.get("delta"));
}
if (delta > revisionNumber) {
throw new URISyntaxException(uri.toASCIIString(),
"Delta must be less or equal to revision-number: "
+ delta + "/" + revisionNumber);
}
if (queryMap.containsKey("meta")) {
metaString = queryMap.get("meta");
}
isUpdateType = (delta != DaapUtil.NULL)
&& (delta < revisionNumber);
// "/databases/id/items" 3 tokens
// "/databases/id/containers" 3 tokens
// "/databases/id/items/id.format" 4 tokens
// "/databases/id/containers/id/items" 5 tokens
if (path.equals("/databases")) {
requestType = DATABASES;
} else if (path.startsWith("/databases")) {
StringTokenizer tok = new StringTokenizer(path, "/");
int count = tok.countTokens();
if (count >= 3) {
String token = tok.nextToken();
if (token.equals("databases") == false) {
throw new URISyntaxException(uri.toASCIIString(),
"Unknown token in path: " + path + " ["
+ token + "]@1");
}
databaseId = DaapUtil.parseUInt(tok.nextToken());
token = tok.nextToken();
if (token.equals("items")) {
requestType = DATABASE_SONGS;
} else if (token.equals("containers")) {
requestType = DATABASE_PLAYLISTS;
} else {
throw new URISyntaxException(uri.toASCIIString(),
"Unknown token in path: " + path + " ["
+ token + "]@2");
}
if (count == 3) {
// do nothing...
} else if (count == 4) {
token = tok.nextToken();
StringTokenizer fileTokenizer = new StringTokenizer(
token, ".");
if (fileTokenizer.countTokens() == 2) {
itemId = DaapUtil.parseUInt(fileTokenizer
.nextToken());
requestType = SONG;
} else {
throw new URISyntaxException(uri
.toASCIIString(),
"Unknown token in path: " + path + " ["
+ token + "]@3");
}
} else if (count == 5) {
containerId = DaapUtil.parseUInt(tok.nextToken());
token = tok.nextToken();
if (token.equals("items")) {
requestType = PLAYLIST_SONGS;
} else {
throw new URISyntaxException(uri
.toASCIIString(),
"Unknown token in path: " + path + " ["
+ token + "@4");
}
} else {
throw new URISyntaxException(uri.toASCIIString(),
"Unknown token in path: " + path + " ["
+ token + "]@5");
}
} else {
throw new URISyntaxException(uri.toASCIIString(),
"Unknown token in path: " + path);
}
}
}
} else {
queryMap = null;
metaString = null;
isUpdateType = false;
requestType = DaapUtil.NULL;
databaseId = DaapUtil.NULL;
containerId = DaapUtil.NULL;
itemId = DaapUtil.NULL;
sessionId = SessionId.INVALID;
revisionNumber = DaapUtil.NULL;
delta = DaapUtil.NULL;
}
}
public void setSessionId(SessionId sessionId) {
this.sessionId = sessionId;
}
/**
* Adds an array of Headers to this requests list of Headers
*
* @return
*/
public void addHeaders(Header[] headers) {
for (Header header : headers) {
this.headers.add(header);
}
}
/**
* Adds a list of headers to this requests list
*
* @return
*/
public void addHeaders(List<? extends Header> headers) {
if (this.headers != headers)
this.headers.addAll(headers);
}
/**
* Adds <code>header</code> to the list
*
* @return
*/
public void addHeader(Header header) {
this.headers.add(header);
}
/**
* Returns the entire list of Headers
*
* @return
*/
public List<Header> getHeaders() {
return headers;
}
/**
* Returns a Header for the key or <code>null</code> if no such Header is in
* the list
*
* @return
*/
public Header getHeader(String key) {
if (headers == null) {
return null;
}
for (Header header : headers) {
if (header.getName().equals(key)) {
return header;
}
}
return null;
}
/**
* Returns the Server reference
*/
public DaapServer<?> getServer() {
return getConnection().getServer();
}
/**
* Returns the associated DaapConnection
*/
public DaapConnection getConnection() {
return connection;
}
/**
* Returns <code>true</code> if this is an unknown request
*
* @return
*/
public boolean isUnknownRequest() {
return (requestType == DaapUtil.NULL);
}
/**
* Returns <code>true</code> if this is a server info request
*
* <p>
* <i>GET /server-info HTTP/1.1</i>
* </p>
*
* @return
*/
public boolean isServerInfoRequest() {
return (requestType == SERVER_INFO);
}
/**
* Returns <code>true</code> if this is a content codes request
*
* <p>
* <i>GET /content-codes HTTP/1.1</i>
* </p>
*
* @return
*/
public boolean isContentCodesRequest() {
return (requestType == CONTENT_CODES);
}
/**
* Returns <code>true</code> if this is a login request
*
* <p>
* <i>GET /login HTTP/1.1</i>
* </p>
*
* @return
*/
public boolean isLoginRequest() {
return (requestType == LOGIN);
}
/**
* Returns <code>true</code> if this is a logout request
*
* <p>
* <i>GET /logout HTTP/1.1</i>
* </p>
*
* @return
*/
public boolean isLogoutRequest() {
return (requestType == LOGOUT);
}
/**
* Returns <code>true</code> if this is an update request
*
* <p>
* <i>GET /update HTTP/1.1</i>
* </p>
*
* @return
*/
public boolean isUpdateRequest() {
return (requestType == UPDATE);
}
/**
* Returns <code>true</code> if this is a resolve request <i>(not
* supported)</i>
*
* <p>
* <i>GET /resolve HTTP/1.1</i>
* </p>
*
* @return
*/
public boolean isResolveRequest() {
return (requestType == RESOLVE);
}
/**
* Returns <code>true</code> if this is a databases request
*
* <p>
* <i>GET /databases HTTP/1.1</i>
* </p>
*
* @return
*/
public boolean isDatabasesRequest() {
return (requestType == DATABASES);
}
/**
* Returns <code>true</code> if this is a database songs request
*
* <p>
* <i>GET /databases/databaseId/items HTTP/1.1</i>
* </p>
*
* @return
*/
public boolean isDatabaseSongsRequest() {
return (requestType == DATABASE_SONGS);
}
/**
* Returns <code>true</code> if this is a database playlists request
*
* <p>
* <i>GET /databases/databaseId/containers HTTP/1.1</i>
* </p>
*
* @return
*/
public boolean isDatabasePlaylistsRequest() {
return (requestType == DATABASE_PLAYLISTS);
}
/**
* Returns <code>true</code> if this is a playlist request
*
* <p>
* <i>GET /databases/databaseId/containers/containerId/items HTTP/1.1</i>
* </p>
*
* @return
*/
public boolean isPlaylistSongsRequest() {
return (requestType == PLAYLIST_SONGS);
}
/**
* Returns <code>true</code> if this is a song request (stream)
*
* <p>
* <i>GET /databases/databaseId/items/itemId.format HTTP/1.1</i>
* </p>
*
* @return
*/
public boolean isSongRequest() {
return (requestType == SONG);
}
/**
* Returns the URI
*
* @return
*/
public URI getUri() {
return uri;
}
/**
* Returns the sessionId
*
* @return
*/
public SessionId getSessionId() {
return sessionId;
}
/**
* Returns the revision-number
*
* @return
*/
public int getRevisionNumber() {
return revisionNumber;
}
/**
* What's delta? Delta is the difference between the current revision of the
* Library (Server) and the latest revision of which iTunes (Client) knows.
*
* @return
*/
public int getDelta() {
return delta;
}
/**
* Returns the keys of the requested meta data as List. Note: this data
* isn't used to generate a response. iTunes is very fussy about the return
* order of some items and it would be to expensive bring the List into the
* correct order.
*
* @return
*/
public List<String> getMeta() {
// parse only if required...
if (meta == null && metaString != null) {
meta = DaapUtil.parseMeta(metaString);
metaString = null;
}
if (meta != null) {
return Collections.unmodifiableList(meta);
}
return Collections.emptyList();
}
/**
* Returns the databaseId
*
* @return
*/
public long getDatabaseId() {
return databaseId;
}
/**
* Returns the containerId
*
* @return
*/
public long getContainerId() {
return containerId;
}
/**
* Returns the itemId
*
* @return
*/
public long getItemId() {
return itemId;
}
/**
* Returns <code>true</code> if databaseId is set (i.e. something else than
* {@see DaapUtil.UNDEF_VALUE}).
*
* @return
*/
public boolean isDatabaseIdSet() {
return (databaseId != DaapUtil.NULL);
}
/**
* Returns <code>true</code> if containerId is set (i.e. something else than
* {@see DaapUtil.UNDEF_VALUE}).
*
* @return
*/
public boolean isContainerIdSet() {
return (containerId != DaapUtil.NULL);
}
/**
* Returns <code>true</code> if itemId is set (i.e. something else than
* {@see DaapUtil.UNDEF_VALUE}).
*
* @return
*/
public boolean isItemIdSet() {
return (itemId != DaapUtil.NULL);
}
/**
* Returns the raw request time.
*
* @return
*/
public int getRequestType() {
return requestType;
}
/**
* Returns the query of this requests URI as a Map
*
* @return
*/
public Map<String, String> getQueryMap() {
if (queryMap != null) {
return Collections.unmodifiableMap(queryMap);
}
return Collections.emptyMap();
}
/**
* Returns <code>true</code> if this is a "fake" request generated by the
* server. It's needed to bypass some security checks of
* DaapRequestProcessor.
*
* @return
*/
public boolean isServerSideRequest() {
return isServerSideRequest;
}
/**
* Returns <code>true</code> if this request is an update request. Except
* for the first request it's always update type request.
*
* @return
*/
public boolean isUpdateType() {
return isUpdateType;
}
/**
* Returns <code>true</code> if client accepts GZIP encoding.
*
* @return
*/
public boolean isGZIPSupported() {
Header header = getHeader("Accept-Encoding");
return header != null && header.getValue().equalsIgnoreCase("gzip");
}
public boolean isKeepConnectionAlive() {
Header header = getHeader("Connection");
return header != null
&& header.getValue().equalsIgnoreCase("keep-alive");
}
/**
*
* @return
*/
public Library getLibrary() {
return connection.getServer().getLibrary();
}
/**
*
* @return
*/
public Library getHeadLibrary() {
return connection.getFirstInQueue();
}
/**
*
* @return
*/
public Library nextLibrary() {
return connection.nextLibrary(this);
}
public String toString() {
StringBuffer buffer = new StringBuffer();
if (isServerSideRequest)
buffer.append("ServerSideRequest: ").append(getRevisionNumber())
.append(", ").append(getDelta()).append("\n");
if (uri != null)
buffer.append(uri).append("\n");
if (headers != null) {
for (int i = 0; i < headers.size(); i++)
buffer.append(headers.get(i));
}
return buffer.toString();
}
}