-module(ts_api). -compile(export_all). -include("ts_db_records.hrl"). -include("yaws_api.hrl"). out(YArg) -> % retreive the session data Session = ts_api_session:get_session(YArg), %get the app mod data PathString = YArg#arg.appmoddata, % split the path PathElements = case PathString of undefined -> []; %handle no end slash: /ts_api _Any -> string:tokens(PathString, "/") end, % process the request case catch dispatch_request(YArg, Session, PathElements) of {'EXIT', Err} -> % TODO: log error internally error_logger:error_report("TimeStamper: ~p", [Err]), make_json_500(YArg, Err); Other -> Other end. % ================================== % % ======== DISPATCH METHODS ======== % % ================================== % %% Entry point to the TimeStamper API dispatch system dispatch_request(YArg, _Session, []) -> make_json_404(YArg, [{see_docs, "/ts_api_doc"}]); dispatch_request(YArg, Session, [H|T]) -> case {Session, H} of {_, "login"} -> do_login(YArg); {_, "logout"} -> do_logout(YArg); {"not_logged_in", _} -> make_json_401(YArg); {"session_expired", _} -> make_json_401(YArg, [{error, "session expired"}]); {_S, "app"} -> dispatch_app(YArg, Session, T); {_S, "users"} -> dispatch_user(YArg, Session, T); {_S, "timelines"} -> dispatch_timeline(YArg, Session, T); {_S, "entries"} -> dispatch_entry(YArg, Session, T); {_S, _Other} -> make_json_404(YArg, [{see_docs, "/ts_api_doc/"}]) end. % -------- Dispatch for /app -------- % dispatch_app(YArg, Session, Params) -> HTTPMethod = (YArg#arg.req)#http_request.method, case {HTTPMethod, Params} of {'GET', ["user_summary", UsernameStr]} -> case {Session#ts_api_session.username, UsernameStr} of {Username, Username} -> get_user_summary(YArg, Username); _ -> make_json_401(YArg) end; {_BadMethod, ["user_summary", _UsernameStr]} -> make_json_405(YArg, [{see_docs, "/ts_api_docs/app.html"}]); _Other -> make_json_404(YArg, [{see_docs, "/ts_api_docs/app.html"}]) end. % -------- Dispatch for /user -------- % dispatch_user(YArg, Session, []) -> dispatch_user(YArg, Session, [Session#ts_api_session.username]); dispatch_user(YArg, Session, [Username]) -> HTTPMethod = (YArg#arg.req)#http_request.method, % compare to the logged-in user case {HTTPMethod, Session#ts_api_session.username} of {'GET', Username} -> get_user(YArg, Username); {_BadMethod, Username} -> make_json_405(YArg, [{see_docs, "/ts_api_doc/users.html"}]); _Other -> make_json_401(YArg, [{see_docs, "/ts_api_doc/users.html"}]) end. % -------- Dispatch for /timeline -------- % dispatch_timeline(YArg, _Session, []) -> make_json_404(YArg, [{see_docs, "/ts_api_doc/timelines.html"}]); dispatch_timeline(YArg, Session, [Username|_T] = PathElements) -> case Session#ts_api_session.username of Username -> dispatch_timeline(YArg, PathElements); _Other -> make_json_404(YArg, [{see_docs, "/ts_api_doc/users.html"}]) end. % just username, list timelines dispatch_timeline(YArg, [Username]) -> HTTPMethod = (YArg#arg.req)#http_request.method, case HTTPMethod of 'GET' -> list_timelines(YArg, Username); _Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/timelines.html"}]) end; dispatch_timeline(YArg, [Username, TimelineId]) -> HTTPMethod = (YArg#arg.req)#http_request.method, case HTTPMethod of 'GET' -> get_timeline(YArg, Username, TimelineId); 'POST' -> post_timeline(YArg, Username, TimelineId); 'PUT' -> put_timeline(YArg, Username, TimelineId); 'DELETE' -> delete_timeline(YArg, Username, TimelineId); _Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/timelines.html"}]) end; dispatch_timeline(YArg, _Other) -> make_json_404(YArg, [{see_docs, "/ts_api_doc/timelines.html"}]). % -------- Dispatch for /entry -------- % dispatch_entry(YArg, _Session, []) -> make_json_404(YArg, [{see_docs, "/ts_aip_doc/entries.html"}]); dispatch_entry(YArg, Session, [Username|_T] = PathElements) -> case Session#ts_api_session.username of Username -> dispatch_entry(YArg, PathElements); _Other -> make_json_404(YArg, [{see_docs, "/ts_api_doc/entries.html"}]) end. dispatch_entry(YArg, [Username, TimelineId]) -> HTTPMethod = (YArg#arg.req)#http_request.method, case HTTPMethod of 'GET' -> list_entries(YArg, Username, TimelineId); 'POST' -> post_entry(YArg, Username, TimelineId); _Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/entries.html"}]) end; dispatch_entry(YArg, [Username, TimelineId, UrlEntryId]) -> EntryId = list_to_integer(UrlEntryId), % TODO: catch non-numbers HTTPMethod = (YArg#arg.req)#http_request.method, case HTTPMethod of 'GET' -> get_entry(YArg, Username, TimelineId, EntryId); 'PUT' -> put_entry(YArg, Username, TimelineId, EntryId); 'DELETE' -> delete_entry(YArg, Username, TimelineId, EntryId); _Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/entries.html"}]) end; dispatch_entry(YArg, _Other) -> make_json_404(YArg, [{see_docs, "/ts_api_doc/entries.html"}]). % ============================== % % ======== IMPLEMENTATION ====== % % ============================== % do_login(YArg) -> EJSON = parse_json_body(YArg), {struct, Fields} = EJSON, case {lists:keyfind(username, 1, Fields), lists:keyfind(password, 1, Fields)} of % username and password found {{username, Username}, {password, Password}} -> % check the uname, password case ts_user:check_credentials(Username, Password) of % they are good true -> {CookieVal, _Session} = ts_api_session:new(Username), [{content, "application/json", json:encode({struct, [{status, "ok"}]})}, {header, {set_cookie, io_lib:format( "ts_api_session=~s; Path=/ts_api; httponly", [CookieVal])}}]; % they are not good false -> make_json_401(YArg, [{error, "bad username/password combination"}]) end; _Other -> make_json_400(YArg, [{see_docs, "/ts_api_doc/login.html"}]) end. do_logout(YArg) -> Cookie = (YArg#arg.headers)#headers.cookie, CookieVal = yaws_api:find_cookie_val("ts_api_session", Cookie), ts_api_session:logout(CookieVal), {status, 200}. get_user_summary(YArg, Username) -> case ts_user:lookup(Username) of no_record -> make_json_404(YArg); User -> EJSONUser = ts_json:record_to_ejson(User), Timelines = ts_timeline:list(Username, 0, 100), EJSONTimelines = {array, lists:map(fun ts_json:record_to_ejson/1, Timelines)}, JSONResp = json:encode({struct, [{user, EJSONUser}, {timelines, EJSONTimelines} ]}), {content, "application/json", JSONResp} end. get_user(YArg, Username) -> case ts_user:lookup(Username) of no_record -> make_json_404(YArg); User -> make_json_200(YArg, User) end. list_timelines(YArg, Username) -> % pull out the POST data QueryData = yaws_api:parse_query(YArg), % read or default the Start Start = case lists:keyfind(start, 1, QueryData) of {start, StartVal} -> list_to_integer(StartVal); false -> 0 end, % read or default the Length Length = case lists:keyfind(length, 1, QueryData) of {length, LengthVal} -> erlang:min(list_to_integer(LengthVal), 50); false -> 50 end, % list the timelines from the database Timelines = ts_timeline:list(Username, Start, Length), % convert them all to their EJSON form EJSONTimelines = {array, lists:map(fun ts_json:record_to_ejson/1, Timelines)}, % create resposne JSONResponse = json:encode(EJSONTimelines), % return response {content, "application/json", JSONResponse}. get_timeline(YArg, Username, TimelineId) -> % look for timeline case ts_timeline:lookup(Username, TimelineId) of % no such timeline, return 404 no_record -> make_json_404(YArg, [{error, "no such timeline"}]); % return the timeline data Timeline -> make_json_200(YArg, Timeline) end. post_timeline(YArg, Username, TimelineId) -> % parse the request body EJSON = parse_json_body(YArg), % parse into a Timeline record TR = try ts_json:ejson_to_record(#ts_timeline{}, EJSON) catch _:InputError -> error_logger:error_report("Bad input: ~p", [InputError]), throw(make_json_400(YArg)) end, % set username and timeline id NewRecord = TR#ts_timeline{ref = {Username, TimelineId}}, % insert into the database case ts_timeline:new(NewRecord) of % record created ok -> [{status, 201}, make_json_200(YArg, NewRecord)]; % will not create, record exists {error, {record_exists, ExistingRecord}} -> JSONResponse = json:encode(ts_json:record_to_ejson(ExistingRecord)), {content, "application/json", JSONResponse}; Error -> error_logger:error_report("Unable to create a new timeline: ~p", [Error]), make_json_500(YArg, Error) end. put_timeline(YArg, Username, TimelineId) -> % parse the POST data EJSON = parse_json_body(YArg), %{struct, Fields} = EJSON, % parse into a timeline record TR = try ts_json:ejson_to_record(#ts_timeline{}, EJSON) catch _:InputError -> error_logger:error_report("Bad input: ~p", [InputError]), throw(make_json_400(YArg)) end, % not supported right now, would require changing all the entry keys % check to see if they are changing the timeline id %NewTimelineId = case lists:keyfind(1, timeline_id, Fields) of % {timeline_id, Field} -> Field; % false -> TimelineId end, % set username and timeline id NewRecord = TR#ts_timeline{ref = {Username, TimelineId}}, case ts_timeline:update(NewRecord) of ok -> make_json_200(YArg, NewRecord); no_record -> make_json_404(YArg, [{error, "no such timeline"}, {see_docs, "/ts_api_doc/timelines.html#POST"}]); Error -> error_logger:error_report("Unable update timeline: ~p", [Error]), make_json_500(YArg, Error) end. delete_timeline(_YArg, _Username, _TimelineId) -> {status, 405}. list_entries(YArg, Username, TimelineId) -> % pull out the POST data QueryData = yaws_api:parse_query(YArg), % first determine if we are listing by date case {ts_timeline:lookup(Username, TimelineId), lists:keyfind("byDate", 1, QueryData)} of {no_record, _ByDateField} -> make_json_404( [{error, "no such timeline"}, {see_docs, "/ts_api_doc/entries.html#LIST"}]); % listing by date range {Timeline, {"byDate", "true"}} -> % look for the start date; default to the beginning of the timeline StartDate = case lists:keyfind("startDate", 1, QueryData) of % TODO: error handling if the date is badly formatted {startDate, StartDateVal} -> ts_json:decode_date(StartDateVal); false -> Timeline#ts_timeline.created end, % look for end date; default to right now EndDate = case lists:keyfind("endDate", 1, QueryData) of % TODO: error handling if the date is badly formatted {endDate, EndDateVal} -> ts_json:decode_date(EndDateVal); false -> calendar:now_to_universal_time(erlang:now()) end, % read sort order and list entries Entries = case lists:keyfind("order", 1, QueryData) of % descending sort order {order, "desc"} -> ts_entry:list_desc( {Username, TimelineId}, StartDate, EndDate); % ascending order--{other, asc}--and default _Other -> ts_entry:list_asc( {Username, TimelineId}, StartDate, EndDate) end, EJSONEntries = {array, lists:map( fun ts_json:record_to_ejson/1, Entries)}, JSONResponse = json:encode(EJSONEntries), {content, "application/json", JSONResponse}; % listing by table position _Other -> % read or default the Start Start = case lists:keyfind("start", 1, QueryData) of {"start", StartVal} -> list_to_integer(StartVal); false -> 0 end, % read or default the Length Length = case lists:keyfind("length", 1, QueryData) of {"length", LengthVal} -> erlang:min(list_to_integer(LengthVal), 500); false -> 50 end, % read sort order and list entries Entries = case lists:keyfind("order", 1, QueryData) of {"order", "desc"} -> ts_entry:list_desc( {Username, TimelineId}, Start, Length); _UnknownOrder -> ts_entry:list_asc( {Username, TimelineId}, Start, Length) end, EJSONEntries = {array, lists:map( fun ts_json:record_to_ejson/1, Entries)}, JSONResponse = json:encode(EJSONEntries), {content, "application/json", JSONResponse} end. get_entry(YArg, Username, TimelineId, EntryId) -> case ts_entry:lookup(Username, TimelineId, EntryId) of % no such entry no_record -> make_json_404(YArg, [{error, "no such entry"}]); % return the entry data Entry -> make_json_200(YArg, Entry) end. post_entry(YArg, Username, TimelineId) -> % parse the request body EJSON = parse_json_body(YArg), % parse into ts_entry record ER = try ts_json:ejson_to_record(#ts_entry{}, EJSON) catch _:InputError -> error_logger:error_report("Bad input: ~p", [InputError]), throw(make_json_400(YArg)) end, % set username and timeline id NewRecord = ER#ts_entry{ref = {Username, TimelineId, undef}}, case ts_entry:new(NewRecord) of % record created {ok, CreatedRecord} -> [{status, 201}, make_json_200(YArg, CreatedRecord)]; % will not create, record exists {error, {record_exists, ExistingRecord}} -> JSONResponse = json:encode(ts_json:record_to_ejson(ExistingRecord)), {content, "application/json", JSONResponse}; OtherError -> error_logger:error_report("TimeStamper: Could not create entry: ~p", [OtherError]), make_json_500(YArg, OtherError) end. put_entry(YArg, Username, TimelineId, EntryId) -> % parse the POST data EJSON = parse_json_body(YArg), % parse into ts_entry record ER = try ts_json:ejson_to_record(#ts_entry{}, EJSON) catch _:InputError -> error_logger:error_report("Bad input: ~p", [InputError]), throw(make_json_400(YArg)) end, % set uername, timeline id, and entry id NewRecord = ER#ts_entry{ref = {Username, TimelineId, EntryId}}, case ts_entry:update(NewRecord) of ok -> make_json_200(YArg, NewRecord); no_record -> make_json_404(YArg, [{error, "no such entry"}, {see_docs, "/ts_api_doc/entries.html#POST"}]); Error -> error_logger:error_report("TimeStamper: Unable to update entry: ~p", [Error]), make_json_500(YArg, Error) end. delete_entry(YArg, Username, TimelineId, EntryId) -> % find the record to delete case ts_entry:lookup(Username, TimelineId, EntryId) of no_record -> make_json_404(YArg); Record -> % try to delete case ts_entry:delete(Record) of ok -> {status, 200}; Error -> error_logger:error_report("Error occurred deleting entry record: ~p", [Error]), make_json_500(YArg, Error) end end. % ============================== % % ======== UTIL METHODS ======== % % ============================== % parse_json_body(YArg) -> case catch json:decode([], binary_to_list(YArg#arg.clidata)) of {done, {ok, EJSON}, _} -> EJSON; Error -> % TODO: log error internally error_logger:error_report("Error parsing JSON request body: ~p", [Error]), throw(make_json_400(YArg)) end. %% Create a JSON 200 response. make_json_200(_YArg, Record) -> JSONResponse = json:encode(ts_json:record_to_ejson(Record)), {content, "application/json", JSONResponse}. make_json_400(YArg) -> make_json_400(YArg, []). make_json_400(_YArg, Fields) -> F1 = case lists:keyfind(status, 1, Fields) of false -> Fields ++ [{status, "bad request"}]; _Else -> Fields end, [{status, 400}, {content, "application/json", json:encode({struct, F1})}]. make_json_401(YArg) -> make_json_401(YArg, []). make_json_401(_YArg, Fields) -> % add default status if not provided F1 = case lists:keyfind(status, 1, Fields) of false -> Fields ++ [{status, "unauthorized"}]; _Else -> Fields end, [{status, 401}, {content, "application/json", json:encode({struct, F1})}]. %% Create a JSON 404 response. make_json_404(YArg) -> make_json_404(YArg, []). make_json_404(YArg, Fields) -> % add default status if not provided F1 = case lists:keyfind(status, 1, Fields) of false -> Fields ++ [{status, "not found"}]; _Else -> Fields end, % add the path they requested F2 = F1 ++ [{path, element(2, (YArg#arg.req)#http_request.path)}], [{status, 404}, {content, "application/json", json:encode({struct, F2})}]. make_json_405(YArg) -> make_json_405(YArg, []). make_json_405(YArg, Fields) -> % add default status if not provided F1 = case lists:keyfind(status, 1, Fields) of false -> Fields ++ [{status, "method not allowed"}]; _Else -> Fields end, % add the path they requested % F2 = F1 ++ [{path, io_lib:format("~s", [(YArg#arg.req)#http_request.path])}], [{status, 405}, {content, "application/json", json:encode({struct, F1})}]. make_json_500(_YArg, Error) -> EJSON = {struct, [ {status, "internal server error"}, {error, io_lib:format("~s", [Error])}]}, [{status, 500}, {content, "application/json", json:encode(EJSON)}]. make_json_500(_YArg) -> EJSON = {struct, [ {status, "internal server error"}]}, [{status, 500}, {content, "application/json", json:encode(EJSON)}].