import 'dart:convert'; import 'dart:io'; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; import 'package:musicus_database/musicus_database.dart'; /// A user of the Musicus API. class User { /// Username. final String name; /// An optional email address. final String email; /// The user's password. final String password; User({ this.name, this.email, this.password, }); Map toJson() => { 'name': name, 'email': email, 'password': password, }; } /// A simple http client for the Musicus server. class MusicusClient { /// URI scheme to use for the connection. /// /// This will be used as the scheme parameter when creating Uri objects. final String scheme; /// The host name of the Musicus server to connect to. /// /// This will be used as the host parameter when creating Uri objects. final String host; /// This will be used as the port parameter when creating Uri objects. final int port; /// Base path to the root location of the Musicus API. final String basePath; User _user; /// The user to login. User get user => _user; set user(User user) { _user = user; _token = null; } final _client = http.Client(); /// The last retrieved access token. String _token; MusicusClient({ this.scheme = 'https', @required this.host, this.port = 443, this.basePath, User user, }) : assert(scheme != null), assert(port != null), assert(host != null), _user = user; /// Create an URI using member variables and parameters. Uri createUri({ @required String path, Map params, }) { return Uri( scheme: scheme, host: host, port: port, path: basePath != null ? basePath + path : path, queryParameters: params, ); } /// Register a new user. /// /// This will return true, if the action was successful. Subsequent requests /// will automatically be made as the new user. Future register(User newUser) async { final response = await _client.post( createUri( path: '/register', ), headers: {'Content-Type': 'application/json'}, body: jsonEncode(newUser.toJson()), ); if (response.statusCode == HttpStatus.ok) { _user = newUser; _token = null; return true; } else { return false; } } /// Retrieve an access token for [user]. /// /// The token will land in [_token]. If the login failed, a /// [MusicusLoginFailedException] will be thrown. Future _login() async { final response = await _client.post( createUri( path: '/login', ), headers: {'Content-Type': 'application/json'}, body: jsonEncode(user.toJson()), ); if (response.statusCode == HttpStatus.ok) { _token = response.body; } else { throw MusicusLoginFailedException(); } } /// Make a request with authorization. /// /// This will ensure, that the request will be made with a valid /// authorization header. If [user] is null, this will throw a /// [MusicusNotLoggedInException]. If it is neccessary, this will login the /// user and throw a [MusicusLoginFailedException] if that failed. If the /// user is not authorized to perform the requested action, this will throw /// a [MusicusNotAuthorizedException]. Future _authorized(String method, Uri uri, {Map headers, String body}) async { if (_user != null) { Future _request() async { final request = http.Request(method, uri); request.headers.addAll(headers); request.headers['Authorization'] = 'Bearer $_token'; request.body = body; return await http.Response.fromStream(await _client.send(request)); } http.Response response; if (_token != null) { response = await _request(); if (response.statusCode == HttpStatus.unauthorized) { await _login(); response = await _request(); } } else { await _login(); response = await _request(); } if (response.statusCode == HttpStatus.forbidden) { throw MusicusNotAuthorizedException(); } else { return response; } } else { throw MusicusNotLoggedInException(); } } /// Get a list of persons. /// /// You can get another page using the [page] parameter. If a non empty /// [search] string is provided, the persons will get filtered based on that /// string. Future> getPersons([int page, String search]) async { final params = {}; if (page != null) { params['p'] = page.toString(); } if (search != null) { params['s'] = search; } final response = await _client.get(createUri( path: '/persons', params: params, )); final json = jsonDecode(response.body); return json.map((j) => Person.fromJson(j)).toList(); } /// Get a person by ID. Future getPerson(int id) async { final response = await _client.get(createUri( path: '/persons/$id', )); final json = jsonDecode(response.body); return Person.fromJson(json); } /// Delete a person by ID. Future deletePerson(int id) async { await _authorized( 'DELETE', createUri( path: '/persons/$id', ), ); } /// Create or update a person. /// /// Returns true, if the operation was successful. Future putPerson(Person person) async { final response = await _authorized( 'PUT', createUri( path: '/persons/${person.id}', ), headers: {'Content-Type': 'application/json'}, body: jsonEncode(person.toJson()), ); return response.statusCode == HttpStatus.ok; } /// Get a list of instruments. /// /// You can get another page using the [page] parameter. If a non empty /// [search] string is provided, the results will get filtered based on that /// string. Future> getInstruments([int page, String search]) async { final params = {}; if (page != null) { params['p'] = page.toString(); } if (search != null) { params['s'] = search; } final response = await _client.get(createUri( path: '/instruments', params: params, )); final json = jsonDecode(response.body); return json.map((j) => Instrument.fromJson(j)).toList(); } /// Get an instrument by ID. Future getInstrument(int id) async { final response = await _client.get(createUri( path: '/instruments/$id', )); final json = jsonDecode(response.body); return Instrument.fromJson(json); } /// Create or update an instrument. /// /// Returns true, if the operation was successful. Future putInstrument(Instrument instrument) async { final response = await _authorized( 'PUT', createUri( path: '/instruments/${instrument.id}', ), headers: {'Content-Type': 'application/json'}, body: jsonEncode(instrument.toJson()), ); return response.statusCode == HttpStatus.ok; } /// Delete an instrument by ID. Future deleteInstrument(int id) async { await _authorized( 'DELETE', createUri( path: '/instruments/$id', ), ); } /// Get a list of works written by the person with the ID [personId]. /// /// You can get another page using the [page] parameter. If a non empty /// [search] string is provided, the results will get filtered based on that /// string. Future> getWorks(int personId, [int page, String search]) async { final params = {}; if (page != null) { params['p'] = page.toString(); } if (search != null) { params['s'] = search; } final response = await _client.get(createUri( path: '/persons/$personId/works', params: params, )); final json = jsonDecode(response.body); return json.map((j) => WorkInfo.fromJson(j)).toList(); } /// Get a work by ID. Future getWork(int id) async { final response = await _client.get(createUri( path: '/works/$id', )); final json = jsonDecode(response.body); return WorkInfo.fromJson(json); } /// Delete a work by ID. Future deleteWork(int id) async { await _authorized( 'DELETE', createUri( path: '/works/$id', ), ); } /// Get a list of recordings of the work with the ID [workId]. /// /// You can get another page using the [page] parameter. Future> getRecordings(int workId, [int page]) async { final params = {}; if (page != null) { params['p'] = page.toString(); } final response = await _client.get(createUri( path: '/works/$workId/recordings', params: params, )); final json = jsonDecode(response.body); return json.map((j) => RecordingInfo.fromJson(j)).toList(); } /// Create or update a work. /// /// Returns true, if the operation was successful. Future putWork(WorkInfo workInfo) async { final response = await _authorized( 'PUT', createUri( path: '/works/${workInfo.work.id}', ), headers: {'Content-Type': 'application/json'}, body: jsonEncode(workInfo.toJson()), ); return response.statusCode == HttpStatus.ok; } /// Get a list of ensembles. /// /// You can get another page using the [page] parameter. If a non empty /// [search] string is provided, the results will get filtered based on that /// string. Future> getEnsembles([int page, String search]) async { final params = {}; if (page != null) { params['p'] = page.toString(); } if (search != null) { params['s'] = search; } final response = await _client.get(createUri( path: '/ensembles', params: params, )); final json = jsonDecode(response.body); return json.map((j) => Ensemble.fromJson(j)).toList(); } /// Get an ensemble by ID. Future getEnsemble(int id) async { final response = await _client.get(createUri( path: '/ensembles/$id', )); final json = jsonDecode(response.body); return Ensemble.fromJson(json); } /// Create or update an ensemble. /// /// Returns true, if the operation was successful. Future putEnsemble(Ensemble ensemble) async { final response = await _authorized( 'PUT', createUri( path: '/ensembles/${ensemble.id}', ), headers: {'Content-Type': 'application/json'}, body: jsonEncode(ensemble.toJson()), ); return response.statusCode == HttpStatus.ok; } /// Delete an ensemble by ID. Future deleteEnsemble(int id) async { await _authorized( 'DELETE', createUri( path: '/ensembles/$id', ), ); } /// Get a recording by ID. Future getRecording(int id) async { final response = await _client.get(createUri( path: '/recordings/$id', )); final json = jsonDecode(response.body); return RecordingInfo.fromJson(json); } /// Create or update a recording. /// /// Returns true, if the operation was successful. Future putRecording(RecordingInfo recordingInfo) async { final response = await _authorized( 'PUT', createUri( path: '/recordings/${recordingInfo.recording.id}', ), headers: {'Content-Type': 'application/json'}, body: jsonEncode(recordingInfo.toJson()), ); return response.statusCode == HttpStatus.ok; } /// Delete a recording by ID. Future deleteRecording(int id) async { await _authorized( 'DELETE', createUri( path: '/recordings/$id', ), ); } /// Close the internal http client. void dispose() { _client.close(); } } class MusicusLoginFailedException implements Exception { MusicusLoginFailedException(); String toString() => 'MusicusLoginFailedException: The username or password ' 'was wrong.'; } class MusicusNotLoggedInException implements Exception { MusicusNotLoggedInException(); String toString() => 'MusicusNotLoggedInException: The user must be logged in to perform ' 'this action.'; } class MusicusNotAuthorizedException implements Exception { MusicusNotAuthorizedException(); String toString() => 'MusicusNotAuthorizedException: The logged in user is not allowed to ' 'perform this action.'; }