#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2009 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Text;
using Nuclex.Networking.Exceptions;
using Nuclex.Support;
using Nuclex.Support.Tracking;
using Nuclex.Support.Scheduling;
namespace Nuclex.Audio.Metadata.Requests {
/// Retrieves or changes the protocol level of a CDDB connection
internal class CddbProtocolLevelRequest :
Request, IAbortable, IProgressReporter {
/// ASCII code for the space character
private const char SP = ' ';
///
/// Delegate used to notify the receiver of the active protocol level
///
/// Active protocol level on the server
public delegate void ProtocolLevelNotificationDelegate(int activeLevel);
/// Triggered when the status of the process changes
public event EventHandler AsyncProgressChanged;
///
/// Initializes a new CDDB protocol level request for retrieval of
/// the current protocol level
///
///
/// Protocol used to communication with the CDDB server
///
public CddbProtocolLevelRequest(
CddbProtocol protocol
) {
this.protocol = protocol;
}
///
/// Initializes a new CDDB protocol level request for changing of
/// the current protocol level
///
///
/// Protocol used to communication with the CDDB server
///
/// New protocol level that will be set
///
/// Callback to be invoked as soon as the new protocol level is set
///
public CddbProtocolLevelRequest(
CddbProtocol protocol, int newLevel, ProtocolLevelNotificationDelegate callback
) {
this.protocol = protocol;
this.newLevel = newLevel;
this.callback = callback;
}
/// Aborts the running process. Can be called from any thread.
///
/// The receiver should honor the abort request and stop whatever it is
/// doing as soon as possible. The method does not impose any requirement
/// on the timeliness of the reaction of the running process, but implementers
/// are advised to not ignore the abort request and urged to try and design
/// their code in such a way that it can be stopped in a reasonable time
/// (eg. within 1 second of the abort request).
///
public void AsyncAbort() {
}
/// Starts the asynchronous execution of the request
public void Start() {
ThreadPool.QueueUserWorkItem(new WaitCallback(execute));
}
///
/// Allows the specific request implementation to re-throw an exception if
/// the background process finished unsuccessfully
///
protected override void ReraiseExceptions() {
if(this.exception != null) {
throw this.exception;
}
}
///
/// Allows the specific request implementation to re-throw an exception if
/// the background process finished unsuccessfully
///
protected override CddbConnection.ServerProtocolLevel GatherResults() {
return this.protocolLevel;
}
/// Triggers the AsyncProgressChanged event
/// New progress to report to the event subscribers
protected virtual void OnProgressAchieved(float progress) {
EventHandler copy = AsyncProgressChanged;
if(copy != null) {
copy(this, new ProgressReportEventArgs(progress));
}
}
/// Called asynchronously to execute the request
/// Not used
private void execute(object state) {
lock(this.protocol.SyncRoot) {
try {
if(this.newLevel.HasValue) {
changeProtocolLevel();
} else {
queryProtocolLevel();
}
}
catch(Exception exception) {
this.exception = exception;
}
}
OnAsyncEnded();
}
/// Changes the CDDB server's active protocol level
private void changeProtocolLevel() {
// Issue the command to the CDDB server
this.protocol.SendLine(string.Format("proto {0}", this.newLevel.Value), 5000);
// Receive the reply from the server
string statusLine = this.protocol.ReceiveLine(5000);
// Obtain the status code returned by the server
int statusCode = CddbProtocol.GetStatusCode(statusLine);
// Decode the server reply
switch(statusCode) {
case 200: {
this.protocolLevel = decodeDetailedProtocolLevel(statusLine);
break;
}
case 201: {
this.protocolLevel = decodeShortProtocolLevel(statusLine);
break;
}
default: {
this.exception = exceptionFromProtocolStatus(
statusCode, statusLine.Substring((statusLine.Length >= 4) ? 4 : 3)
);
return;
}
}
// Because we're pedantic, we'll check whether the server actually reported
// the new protocol level we requested back to us.
if(this.newLevel.Value != this.protocolLevel.ActiveProtocolLevel) {
this.exception = new BadResponseException(
"Server sent a positive response but didn't change the protocol level"
);
} else { // Everything went well
this.callback(this.newLevel.Value);
}
}
/// Queries the server for its active protocol level
private void queryProtocolLevel() {
// Issue the command to the CDDB server
this.protocol.SendLine("proto", 5000);
// Receive the reply from the server
string statusLine = this.protocol.ReceiveLine(5000);
// Obtain the status code returned by the server
int statusCode = CddbProtocol.GetStatusCode(statusLine);
// Decode the server reply
switch(statusCode) {
case 200: {
this.protocolLevel = decodeDetailedProtocolLevel(statusLine);
break;
}
case 201: {
this.protocolLevel = decodeShortProtocolLevel(statusLine);
break;
}
default: {
this.exception = exceptionFromProtocolStatus(
statusCode, statusLine.Substring((statusLine.Length >= 4) ? 4 : 3)
);
break;
}
}
}
/// Decodes a server response containing the detailed protocol level
/// Status line received from the CDDB server
/// The decoded server protocol level
private CddbConnection.ServerProtocolLevel decodeDetailedProtocolLevel(string line) {
// Locate where in the string the current protocol level starts
int currentIndex = -1;
bool hasCurrent =
(line.Length >= 5) &&
(line[3] == SP) &&
((currentIndex = line.IndexOf("current ", 4)) != -1);
if(!hasCurrent) {
throw makeBadResponseException("missing current protocol level indication");
}
// Locate the end of the current protocol level number
int currentNumberEndIndex = -1;
bool hasCurrentNumber =
(line.Length >= currentIndex + 9) &&
((currentNumberEndIndex = line.IndexOf(SP, currentIndex + 9)) != -1);
if(!hasCurrentNumber) {
throw makeBadResponseException("missing current protocol level number");
}
// Locate the position of the maximum supported protocol level in the string
int supportedIndex = -1;
bool hasSupported =
(line.Length >= currentNumberEndIndex + 1) &&
((supportedIndex = line.IndexOf("supported ", currentNumberEndIndex + 1)) != -1);
if(!hasSupported) {
throw makeBadResponseException("missing supported protocol level indication");
}
// All positions are known, try to convert the levels into integers and return them
return new CddbConnection.ServerProtocolLevel(
Convert.ToInt32(
line.Substring(currentIndex + 8, currentNumberEndIndex - currentIndex - 9)
),
Convert.ToInt32(line.Substring(supportedIndex + 10))
);
}
/// Decodes a server response containing the short protocol level
/// Status line received from the CDDB server
/// The decoded server protocol level
private CddbConnection.ServerProtocolLevel decodeShortProtocolLevel(string line) {
// Locate where in the string the current protocol level starts
int nowIndex = -1;
bool hasNow =
(line.Length >= 5) &&
(line[3] == SP) &&
((nowIndex = line.IndexOf("now: ", 4)) != -1);
if(!hasNow) {
throw makeBadResponseException("missing current protocol level indication");
}
// We found the position of the protocol level, now try to convert it into
// an integer and return it
return new CddbConnection.ServerProtocolLevel(
Convert.ToInt32(line.Substring(nowIndex + 5)),
null
);
}
/// Constructs a new BadResponseException
///
/// Detailed reason to show in the exception message in addition to the standard
/// bad response message
///
/// The newly constructed exception
private static BadResponseException makeBadResponseException(string detailedReason) {
return new BadResponseException(
string.Format("Malformed response from CDDB server ({0})", detailedReason)
);
}
///
/// Generates an exception from the status code in the reply to
/// the genre list request
///
/// Status code provided by the server
/// Response returned by the server
///
/// The exception for the server status code or null if the status code
/// indicates success
///
private static Exception exceptionFromProtocolStatus(int statusCode, string message) {
switch(statusCode) {
case 501: {
throw new Exceptions.IllegalProtocolLevelException(message);
}
case 502: {
throw new Exceptions.ProtocolLevelAlreadySetException(message);
}
default: {
return new BadResponseException(
string.Format(
"Bad response from CDDB server: invalid status code '{0}', " +
"server message is '{1}'",
statusCode, message
)
);
}
}
}
/// Exception that has occured during asynchronous processing
private volatile Exception exception;
/// Protocol used to communicate with the CDDB server
private CddbProtocol protocol;
/// Protocol level the CDDB server is currently using
private CddbConnection.ServerProtocolLevel protocolLevel;
/// Either null for query only or new protocol level to set
private int? newLevel;
/// Call to be invoked when the protocol level has changed
private ProtocolLevelNotificationDelegate callback;
}
} // namespace Nuclex.Audio.Metadata.Requests