Generated on 2009-08-09 21:22:56 with etap 0.3.4.
| Name | Total lines | Lines of code | Total coverage | Code coverage | ||||
| couch_httpd_db | ?? | ?? | ?? |
|
....1 % Licensed under the Apache License, Version 2.0 (the "License"); you may not ....2 % use this file except in compliance with the License. You may obtain a copy of ....3 % the License at ....4 % ....5 % http://www.apache.org/licenses/LICENSE-2.0 ....6 % ....7 % Unless required by applicable law or agreed to in writing, software ....8 % distributed under the License is distributed on an "AS IS" BASIS, WITHOUT ....9 % WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the ...10 % License for the specific language governing permissions and limitations under ...11 % the License. ...12 ...13 -module(couch_httpd_db). ...14 -include("couch_db.hrl"). ...15 ...16 -export([handle_request/1, handle_compact_req/2, handle_design_req/2, ...17 db_req/2, couch_doc_open/4,handle_changes_req/2, ...18 update_doc_result_to_json/1, update_doc_result_to_json/2, ...19 handle_design_info_req/2, handle_view_cleanup_req/2]). ...20 ...21 -import(couch_httpd, ...22 [send_json/2,send_json/3,send_json/4,send_method_not_allowed/2, ...23 start_json_response/2,send_chunk/2,end_json_response/1, ...24 start_chunked_response/3, absolute_uri/2]). ...25 ...26 -record(doc_query_args, { ...27 options = [], ...28 rev = nil, ...29 open_revs = [], ...30 show = nil ...31 }). ...32 ...33 % Database request handlers ...34 handle_request(#httpd{path_parts=[DbName|RestParts],method=Method, ...35 db_url_handlers=DbUrlHandlers}=Req)-> ...36 case {Method, RestParts} of ...37 {'PUT', []} -> ...38 create_db_req(Req, DbName); ...39 {'DELETE', []} -> ...40 delete_db_req(Req, DbName); ...41 {_, []} -> ...42 do_db_req(Req, fun db_req/2); ...43 {_, [SecondPart|_]} -> ...44 Handler = couch_util:dict_find(SecondPart, DbUrlHandlers, fun db_req/2), ...45 do_db_req(Req, Handler) ...46 end. ...47 ...48 get_changes_timeout(Req, Resp) -> ...49 DefaultTimeout = list_to_integer( ...50 couch_config:get("httpd", "changes_timeout", "60000")), ...51 case couch_httpd:qs_value(Req, "heartbeat") of ...52 undefined -> ...53 case couch_httpd:qs_value(Req, "timeout") of ...54 undefined -> ...55 {DefaultTimeout, fun() -> stop end}; ...56 TimeoutList -> ...57 {lists:min([DefaultTimeout, list_to_integer(TimeoutList)]), ...58 fun() -> stop end} ...59 end; ...60 "true" -> ...61 {DefaultTimeout, fun() -> send_chunk(Resp, "\n"), ok end}; ...62 TimeoutList -> ...63 {lists:min([DefaultTimeout, list_to_integer(TimeoutList)]), ...64 fun() -> send_chunk(Resp, "\n"), ok end} ...65 end. ...66 ...67 ...68 handle_changes_req(#httpd{method='GET',path_parts=[DbName|_]}=Req, Db) -> ...69 StartSeq = list_to_integer(couch_httpd:qs_value(Req, "since", "0")), ...70 {ok, Resp} = start_json_response(Req, 200), ...71 send_chunk(Resp, "{\"results\":[\n"), ...72 case couch_httpd:qs_value(Req, "feed", "normal") of ...73 ResponseType when ResponseType == "continuous" orelse ResponseType == "longpoll"-> ...74 Self = self(), ...75 {ok, Notify} = couch_db_update_notifier:start_link( ...76 fun({_, DbName0}) when DbName0 == DbName -> ...77 Self ! db_updated; ...78 (_) -> ...79 ok ...80 end), ...81 {Timeout, TimeoutFun} = get_changes_timeout(Req, Resp), ...82 couch_stats_collector:track_process_count(Self, ...83 {httpd, clients_requesting_changes}), ...84 try ...85 keep_sending_changes(Req, Resp, Db, StartSeq, <<"">>, Timeout, TimeoutFun, ResponseType) ...86 after ...87 couch_db_update_notifier:stop(Notify), ...88 get_rest_db_updated() % clean out any remaining update messages ...89 end; ...90 "normal" -> ...91 {ok, {LastSeq, _Prepend}} = ...92 send_changes(Req, Resp, Db, StartSeq, <<"">>), ...93 end_sending_changes(Resp, LastSeq) ...94 end; ...95 ...96 handle_changes_req(#httpd{path_parts=[_,<<"_changes">>]}=Req, _Db) -> ...97 send_method_not_allowed(Req, "GET,HEAD"). ...98 ...99 % waits for a db_updated msg, if there are multiple msgs, collects them. ..100 wait_db_updated(Timeout, TimeoutFun) -> ..101 receive db_updated -> get_rest_db_updated() ..102 after Timeout -> ..103 case TimeoutFun() of ..104 ok -> wait_db_updated(Timeout, TimeoutFun); ..105 stop -> stop ..106 end ..107 end. ..108 ..109 get_rest_db_updated() -> ..110 receive db_updated -> get_rest_db_updated() ..111 after 0 -> updated ..112 end. ..113 ..114 end_sending_changes(Resp, EndSeq) -> ..115 send_chunk(Resp, io_lib:format("\n],\n\"last_seq\":~w}\n", [EndSeq])), ..116 end_json_response(Resp). ..117 ..118 keep_sending_changes(#httpd{user_ctx=UserCtx,path_parts=[DbName|_]}=Req, Resp, Db, StartSeq, Prepend, Timeout, TimeoutFun, ResponseType) -> ..119 {ok, {EndSeq, Prepend2}} = send_changes(Req, Resp, Db, StartSeq, Prepend), ..120 couch_db:close(Db), ..121 if ..122 EndSeq > StartSeq, ResponseType == "longpoll" -> ..123 end_sending_changes(Resp, EndSeq); ..124 true -> ..125 case wait_db_updated(Timeout, TimeoutFun) of ..126 updated -> ..127 {ok, Db2} = couch_db:open(DbName, [{user_ctx, UserCtx}]), ..128 keep_sending_changes(Req, Resp, Db2, EndSeq, Prepend2, Timeout, TimeoutFun, ResponseType); ..129 stop -> ..130 end_sending_changes(Resp, EndSeq) ..131 end ..132 end. ..133 ..134 send_changes(Req, Resp, Db, StartSeq, Prepend0) -> ..135 Style = list_to_existing_atom( ..136 couch_httpd:qs_value(Req, "style", "main_only")), ..137 {FilterFun, EndFilterFun} = make_filter_funs(Req, Db), ..138 try ..139 couch_db:changes_since(Db, Style, StartSeq, ..140 fun([#doc_info{id=Id, high_seq=Seq}|_]=DocInfos, {_, Prepend}) -> ..141 Results0 = [FilterFun(DocInfo) || DocInfo <- DocInfos], ..142 Results = [Result || Result <- Results0, Result /= null], ..143 case Results of ..144 [] -> ..145 {ok, {Seq, Prepend}}; ..146 _ -> ..147 send_chunk(Resp, ..148 [Prepend, ?JSON_ENCODE({[{seq,Seq}, {id, Id}, ..149 {changes,Results}]})]), ..150 {ok, {Seq, <<",\n">>}} ..151 end ..152 end, {StartSeq, Prepend0}) ..153 after ..154 EndFilterFun() ..155 end. ..156 ..157 make_filter_funs(Req, Db) -> ..158 Filter = couch_httpd:qs_value(Req, "filter", ""), ..159 case [list_to_binary(couch_httpd:unquote(Part)) ..160 || Part <- string:tokens(Filter, "/")] of ..161 [] -> ..162 {fun(#doc_info{revs=[#rev_info{rev=Rev}|_]}) -> ..163 {[{rev, couch_doc:rev_to_str(Rev)}]} ..164 end, ..165 fun() -> ok end}; ..166 [DName, FName] -> ..167 DesignId = <<"_design/", DName/binary>>, ..168 #doc{body={Props}} = couch_httpd_db:couch_doc_open(Db, DesignId, nil, []), ..169 Lang = proplists:get_value(<<"language">>, Props, <<"javascript">>), ..170 FilterSrc = couch_util:get_nested_json_value({Props}, [<<"filters">>, FName]), ..171 {ok, Pid} = couch_query_servers:start_filter(Lang, FilterSrc), ..172 FilterFun = fun(DInfo = #doc_info{revs=[#rev_info{rev=Rev}|_]}) -> ..173 {ok, Doc} = couch_db:open_doc(Db, DInfo), ..174 {ok, Pass} = couch_query_servers:filter_doc(Pid, Doc, Req, Db), ..175 case Pass of ..176 true -> ..177 {[{rev, couch_doc:rev_to_str(Rev)}]}; ..178 false -> ..179 null ..180 end ..181 end, ..182 EndFilterFun = fun() -> ..183 couch_query_servers:end_filter(Pid) ..184 end, ..185 {FilterFun, EndFilterFun}; ..186 _Else -> ..187 throw({bad_request, ..188 "filter parameter must be of the form `designname/filtername`"}) ..189 end. ..190 ..191 handle_compact_req(#httpd{method='POST',path_parts=[DbName,_,Id|_]}=Req, _Db) -> ..192 ok = couch_view_compactor:start_compact(DbName, Id), ..193 send_json(Req, 202, {[{ok, true}]}); ..194 ..195 handle_compact_req(#httpd{method='POST'}=Req, Db) -> ..196 ok = couch_db:start_compact(Db), ..197 send_json(Req, 202, {[{ok, true}]}); ..198 ..199 handle_compact_req(Req, _Db) -> ..200 send_method_not_allowed(Req, "POST"). ..201 ..202 handle_view_cleanup_req(#httpd{method='POST'}=Req, Db) -> ..203 % delete unreferenced index files ..204 ok = couch_view:cleanup_index_files(Db), ..205 send_json(Req, 202, {[{ok, true}]}); ..206 ..207 handle_view_cleanup_req(Req, _Db) -> ..208 send_method_not_allowed(Req, "POST"). ..209 ..210 ..211 handle_design_req(#httpd{ ..212 path_parts=[_DbName,_Design,_DesName, <<"_",_/binary>> = Action | _Rest], ..213 design_url_handlers = DesignUrlHandlers ..214 }=Req, Db) -> ..215 Handler = couch_util:dict_find(Action, DesignUrlHandlers, fun db_req/2), ..216 Handler(Req, Db); ..217 ..218 handle_design_req(Req, Db) -> ..219 db_req(Req, Db). ..220 ..221 handle_design_info_req(#httpd{ ..222 method='GET', ..223 path_parts=[_DbName, _Design, DesignName, _] ..224 }=Req, Db) -> ..225 DesignId = <<"_design/", DesignName/binary>>, ..226 {ok, GroupInfoList} = couch_view:get_group_info(Db, DesignId), ..227 send_json(Req, 200, {[ ..228 {name, DesignName}, ..229 {view_index, {GroupInfoList}} ..230 ]}); ..231 ..232 handle_design_info_req(Req, _Db) -> ..233 send_method_not_allowed(Req, "GET"). ..234 ..235 create_db_req(#httpd{user_ctx=UserCtx}=Req, DbName) -> ..236 ok = couch_httpd:verify_is_server_admin(Req), ..237 case couch_server:create(DbName, [{user_ctx, UserCtx}]) of ..238 {ok, Db} -> ..239 couch_db:close(Db), ..240 DocUrl = absolute_uri(Req, "/" ++ couch_util:url_encode(DbName)), ..241 send_json(Req, 201, [{"Location", DocUrl}], {[{ok, true}]}); ..242 Error -> ..243 throw(Error) ..244 end. ..245 ..246 delete_db_req(#httpd{user_ctx=UserCtx}=Req, DbName) -> ..247 ok = couch_httpd:verify_is_server_admin(Req), ..248 case couch_server:delete(DbName, [{user_ctx, UserCtx}]) of ..249 ok -> ..250 send_json(Req, 200, {[{ok, true}]}); ..251 Error -> ..252 throw(Error) ..253 end. ..254 ..255 do_db_req(#httpd{user_ctx=UserCtx,path_parts=[DbName|_]}=Req, Fun) -> ..256 case couch_db:open(DbName, [{user_ctx, UserCtx}]) of ..257 {ok, Db} -> ..258 try ..259 Fun(Req, Db) ..260 after ..261 catch couch_db:close(Db) ..262 end; ..263 Error -> ..264 throw(Error) ..265 end. ..266 ..267 db_req(#httpd{method='GET',path_parts=[_DbName]}=Req, Db) -> ..268 {ok, DbInfo} = couch_db:get_db_info(Db), ..269 send_json(Req, {DbInfo}); ..270 ..271 db_req(#httpd{method='POST',path_parts=[DbName]}=Req, Db) -> ..272 Doc = couch_doc:from_json_obj(couch_httpd:json_body(Req)), ..273 DocId = couch_util:new_uuid(), ..274 case couch_httpd:qs_value(Req, "batch") of ..275 "ok" -> ..276 % batch ..277 ok = couch_batch_save:eventually_save_doc(Db#db.name, ..278 Doc#doc{id=DocId}, Db#db.user_ctx), ..279 send_json(Req, 202, [], {[ ..280 {ok, true}, ..281 {id, DocId} ..282 ]}); ..283 _Normal -> ..284 % normal ..285 {ok, NewRev} = couch_db:update_doc(Db, Doc#doc{id=DocId}, []), ..286 DocUrl = absolute_uri(Req, ..287 binary_to_list(<<"/",DbName/binary,"/",DocId/binary>>)), ..288 send_json(Req, 201, [{"Location", DocUrl}], {[ ..289 {ok, true}, ..290 {id, DocId}, ..291 {rev, couch_doc:rev_to_str(NewRev)} ..292 ]}) ..293 end; ..294 ..295 ..296 db_req(#httpd{path_parts=[_DbName]}=Req, _Db) -> ..297 send_method_not_allowed(Req, "DELETE,GET,HEAD,POST"); ..298 ..299 db_req(#httpd{method='POST',path_parts=[_,<<"_ensure_full_commit">>]}=Req, Db) -> ..300 % make the batch save ..301 committed = couch_batch_save:commit_now(Db#db.name, Db#db.user_ctx), ..302 {ok, DbStartTime} = couch_db:ensure_full_commit(Db), ..303 send_json(Req, 201, {[ ..304 {ok, true}, ..305 {instance_start_time, DbStartTime} ..306 ]}); ..307 ..308 db_req(#httpd{path_parts=[_,<<"_ensure_full_commit">>]}=Req, _Db) -> ..309 send_method_not_allowed(Req, "POST"); ..310 ..311 db_req(#httpd{method='POST',path_parts=[_,<<"_bulk_docs">>]}=Req, Db) -> ..312 couch_stats_collector:increment({httpd, bulk_requests}), ..313 {JsonProps} = couch_httpd:json_body_obj(Req), ..314 DocsArray = proplists:get_value(<<"docs">>, JsonProps), ..315 case couch_httpd:header_value(Req, "X-Couch-Full-Commit", "false") of ..316 "true" -> ..317 Options = [full_commit]; ..318 _ -> ..319 Options = [] ..320 end, ..321 case proplists:get_value(<<"new_edits">>, JsonProps, true) of ..322 true -> ..323 Docs = lists:map( ..324 fun({ObjProps} = JsonObj) -> ..325 Doc = couch_doc:from_json_obj(JsonObj), ..326 validate_attachment_names(Doc), ..327 Id = case Doc#doc.id of ..328 <<>> -> couch_util:new_uuid(); ..329 Id0 -> Id0 ..330 end, ..331 case proplists:get_value(<<"_rev">>, ObjProps) of ..332 undefined -> ..333 Revs = {0, []}; ..334 Rev -> ..335 {Pos, RevId} = couch_doc:parse_rev(Rev), ..336 Revs = {Pos, [RevId]} ..337 end, ..338 Doc#doc{id=Id,revs=Revs} ..339 end, ..340 DocsArray), ..341 Options2 = ..342 case proplists:get_value(<<"all_or_nothing">>, JsonProps) of ..343 true -> [all_or_nothing|Options]; ..344 _ -> Options ..345 end, ..346 case couch_db:update_docs(Db, Docs, Options2) of ..347 {ok, Results} -> ..348 % output the results ..349 DocResults = lists:zipwith(fun update_doc_result_to_json/2, ..350 Docs, Results), ..351 send_json(Req, 201, DocResults); ..352 {aborted, Errors} -> ..353 ErrorsJson = ..354 lists:map(fun update_doc_result_to_json/1, Errors), ..355 send_json(Req, 417, ErrorsJson) ..356 end; ..357 false -> ..358 Docs = [couch_doc:from_json_obj(JsonObj) || JsonObj <- DocsArray], ..359 {ok, Errors} = couch_db:update_docs(Db, Docs, Options, replicated_changes), ..360 ErrorsJson = ..361 lists:map(fun update_doc_result_to_json/1, Errors), ..362 send_json(Req, 201, ErrorsJson) ..363 end; ..364 db_req(#httpd{path_parts=[_,<<"_bulk_docs">>]}=Req, _Db) -> ..365 send_method_not_allowed(Req, "POST"); ..366 ..367 db_req(#httpd{method='POST',path_parts=[_,<<"_purge">>]}=Req, Db) -> ..368 {IdsRevs} = couch_httpd:json_body_obj(Req), ..369 IdsRevs2 = [{Id, couch_doc:parse_revs(Revs)} || {Id, Revs} <- IdsRevs], ..370 ..371 case couch_db:purge_docs(Db, IdsRevs2) of ..372 {ok, PurgeSeq, PurgedIdsRevs} -> ..373 PurgedIdsRevs2 = [{Id, couch_doc:rev_to_strs(Revs)} || {Id, Revs} <- PurgedIdsRevs], ..374 send_json(Req, 200, {[{<<"purge_seq">>, PurgeSeq}, {<<"purged">>, {PurgedIdsRevs2}}]}); ..375 Error -> ..376 throw(Error) ..377 end; ..378 ..379 db_req(#httpd{path_parts=[_,<<"_purge">>]}=Req, _Db) -> ..380 send_method_not_allowed(Req, "POST"); ..381 ..382 db_req(#httpd{method='GET',path_parts=[_,<<"_all_docs">>]}=Req, Db) -> ..383 all_docs_view(Req, Db, nil); ..384 ..385 db_req(#httpd{method='POST',path_parts=[_,<<"_all_docs">>]}=Req, Db) -> ..386 {Fields} = couch_httpd:json_body_obj(Req), ..387 case proplists:get_value(<<"keys">>, Fields, nil) of ..388 nil -> ..389 ?LOG_DEBUG("POST to _all_docs with no keys member.", []), ..390 all_docs_view(Req, Db, nil); ..391 Keys when is_list(Keys) -> ..392 all_docs_view(Req, Db, Keys); ..393 _ -> ..394 throw({bad_request, "`keys` member must be a array."}) ..395 end; ..396 ..397 db_req(#httpd{path_parts=[_,<<"_all_docs">>]}=Req, _Db) -> ..398 send_method_not_allowed(Req, "GET,HEAD,POST"); ..399 ..400 db_req(#httpd{method='GET',path_parts=[_,<<"_all_docs_by_seq">>]}=Req, Db) -> ..401 #view_query_args{ ..402 start_key = StartKey, ..403 limit = Limit, ..404 skip = SkipCount, ..405 direction = Dir ..406 } = QueryArgs = couch_httpd_view:parse_view_params(Req, nil, map), ..407 ..408 {ok, Info} = couch_db:get_db_info(Db), ..409 CurrentEtag = couch_httpd:make_etag(proplists:get_value(update_seq, Info)), ..410 couch_httpd:etag_respond(Req, CurrentEtag, fun() -> ..411 TotalRowCount = proplists:get_value(doc_count, Info), ..412 FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs, CurrentEtag, Db, ..413 TotalRowCount, #view_fold_helper_funs{ ..414 reduce_count = fun couch_db:enum_docs_since_reduce_to_count/1 ..415 }), ..416 StartKey2 = case StartKey of ..417 nil -> 0; ..418 <<>> -> 100000000000; ..419 {} -> 100000000000; ..420 StartKey when is_integer(StartKey) -> StartKey ..421 end, ..422 {ok, FoldResult} = couch_db:enum_docs_since(Db, StartKey2, Dir, ..423 fun(DocInfo, Offset, Acc) -> ..424 #doc_info{ ..425 id=Id, ..426 high_seq=Seq, ..427 revs=[#rev_info{rev=Rev,deleted=Deleted} | RestInfo] ..428 } = DocInfo, ..429 ConflictRevs = couch_doc:rev_to_strs( ..430 [Rev1 || #rev_info{deleted=false, rev=Rev1} <- RestInfo]), ..431 DelConflictRevs = couch_doc:rev_to_strs( ..432 [Rev1 || #rev_info{deleted=true, rev=Rev1} <- RestInfo]), ..433 Json = { ..434 [{<<"rev">>, couch_doc:rev_to_str(Rev)}] ++ ..435 case ConflictRevs of ..436 [] -> []; ..437 _ -> [{<<"conflicts">>, ConflictRevs}] ..438 end ++ ..439 case DelConflictRevs of ..440 [] -> []; ..441 _ -> [{<<"deleted_conflicts">>, DelConflictRevs}] ..442 end ++ ..443 case Deleted of ..444 true -> [{<<"deleted">>, true}]; ..445 false -> [] ..446 end ..447 }, ..448 FoldlFun({{Seq, Id}, Json}, Offset, Acc) ..449 end, {Limit, SkipCount, undefined, [], nil}), ..450 couch_httpd_view:finish_view_fold(Req, TotalRowCount, {ok, FoldResult}) ..451 end); ..452 ..453 db_req(#httpd{path_parts=[_,<<"_all_docs_by_seq">>]}=Req, _Db) -> ..454 send_method_not_allowed(Req, "GET,HEAD"); ..455 ..456 db_req(#httpd{method='POST',path_parts=[_,<<"_missing_revs">>]}=Req, Db) -> ..457 {JsonDocIdRevs} = couch_httpd:json_body_obj(Req), ..458 JsonDocIdRevs2 = [{Id, [couch_doc:parse_rev(RevStr) || RevStr <- RevStrs]} || {Id, RevStrs} <- JsonDocIdRevs], ..459 {ok, Results} = couch_db:get_missing_revs(Db, JsonDocIdRevs2), ..460 Results2 = [{Id, [couch_doc:rev_to_str(Rev) || Rev <- Revs]} || {Id, Revs} <- Results], ..461 send_json(Req, {[ ..462 {missing_revs, {Results2}} ..463 ]}); ..464 ..465 db_req(#httpd{path_parts=[_,<<"_missing_revs">>]}=Req, _Db) -> ..466 send_method_not_allowed(Req, "POST"); ..467 ..468 db_req(#httpd{method='PUT',path_parts=[_,<<"_admins">>]}=Req, ..469 Db) -> ..470 Admins = couch_httpd:json_body(Req), ..471 ok = couch_db:set_admins(Db, Admins), ..472 send_json(Req, {[{<<"ok">>, true}]}); ..473 ..474 db_req(#httpd{method='GET',path_parts=[_,<<"_admins">>]}=Req, Db) -> ..475 send_json(Req, couch_db:get_admins(Db)); ..476 ..477 db_req(#httpd{path_parts=[_,<<"_admins">>]}=Req, _Db) -> ..478 send_method_not_allowed(Req, "PUT,GET"); ..479 ..480 db_req(#httpd{method='PUT',path_parts=[_,<<"_revs_limit">>]}=Req, ..481 Db) -> ..482 Limit = couch_httpd:json_body(Req), ..483 ok = couch_db:set_revs_limit(Db, Limit), ..484 send_json(Req, {[{<<"ok">>, true}]}); ..485 ..486 db_req(#httpd{method='GET',path_parts=[_,<<"_revs_limit">>]}=Req, Db) -> ..487 send_json(Req, couch_db:get_revs_limit(Db)); ..488 ..489 db_req(#httpd{path_parts=[_,<<"_revs_limit">>]}=Req, _Db) -> ..490 send_method_not_allowed(Req, "PUT,GET"); ..491 ..492 % Special case to enable using an unencoded slash in the URL of design docs, ..493 % as slashes in document IDs must otherwise be URL encoded. ..494 db_req(#httpd{method='GET',mochi_req=MochiReq, path_parts=[DbName,<<"_design/",_/binary>>|_]}=Req, _Db) -> ..495 PathFront = "/" ++ couch_httpd:quote(binary_to_list(DbName)) ++ "/", ..496 RawSplit = regexp:split(MochiReq:get(raw_path),"_design%2F"), ..497 {ok, [PathFront|PathTail]} = RawSplit, ..498 couch_httpd:send_redirect(Req, PathFront ++ "_design/" ++ ..499 mochiweb_util:join(PathTail, "_design%2F")); ..500 ..501 db_req(#httpd{path_parts=[_DbName,<<"_design">>,Name]}=Req, Db) -> ..502 db_doc_req(Req, Db, <<"_design/",Name/binary>>); ..503 ..504 db_req(#httpd{path_parts=[_DbName,<<"_design">>,Name|FileNameParts]}=Req, Db) -> ..505 db_attachment_req(Req, Db, <<"_design/",Name/binary>>, FileNameParts); ..506 ..507 ..508 db_req(#httpd{path_parts=[_, DocId]}=Req, Db) -> ..509 db_doc_req(Req, Db, DocId); ..510 ..511 db_req(#httpd{path_parts=[_, DocId | FileNameParts]}=Req, Db) -> ..512 db_attachment_req(Req, Db, DocId, FileNameParts). ..513 ..514 all_docs_view(Req, Db, Keys) -> ..515 #view_query_args{ ..516 start_key = StartKey, ..517 start_docid = StartDocId, ..518 end_key = EndKey, ..519 limit = Limit, ..520 skip = SkipCount, ..521 direction = Dir ..522 } = QueryArgs = couch_httpd_view:parse_view_params(Req, Keys, map), ..523 {ok, Info} = couch_db:get_db_info(Db), ..524 CurrentEtag = couch_httpd:make_etag(proplists:get_value(update_seq, Info)), ..525 couch_httpd:etag_respond(Req, CurrentEtag, fun() -> ..526 ..527 TotalRowCount = proplists:get_value(doc_count, Info), ..528 StartId = if is_binary(StartKey) -> StartKey; ..529 true -> StartDocId ..530 end, ..531 FoldAccInit = {Limit, SkipCount, undefined, [], nil}, ..532 ..533 case Keys of ..534 nil -> ..535 PassedEndFun = ..536 case Dir of ..537 fwd -> ..538 fun(ViewKey, _ViewId) -> ..539 couch_db_updater:less_docid(EndKey, ViewKey) ..540 end; ..541 rev-> ..542 fun(ViewKey, _ViewId) -> ..543 couch_db_updater:less_docid(ViewKey, EndKey) ..544 end ..545 end, ..546 ..547 FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs, CurrentEtag, Db, ..548 TotalRowCount, #view_fold_helper_funs{ ..549 reduce_count = fun couch_db:enum_docs_reduce_to_count/1, ..550 passed_end = PassedEndFun ..551 }), ..552 AdapterFun = fun(#full_doc_info{id=Id}=FullDocInfo, Offset, Acc) -> ..553 case couch_doc:to_doc_info(FullDocInfo) of ..554 #doc_info{revs=[#rev_info{deleted=false, rev=Rev}|_]} -> ..555 FoldlFun({{Id, Id}, {[{rev, couch_doc:rev_to_str(Rev)}]}}, Offset, Acc); ..556 #doc_info{revs=[#rev_info{deleted=true}|_]} -> ..557 {ok, Acc} ..558 end ..559 end, ..560 {ok, FoldResult} = couch_db:enum_docs(Db, StartId, Dir, ..561 AdapterFun, FoldAccInit), ..562 couch_httpd_view:finish_view_fold(Req, TotalRowCount, {ok, FoldResult}); ..563 _ -> ..564 FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs, CurrentEtag, Db, ..565 TotalRowCount, #view_fold_helper_funs{ ..566 reduce_count = fun(Offset) -> Offset end ..567 }), ..568 KeyFoldFun = case Dir of ..569 fwd -> ..570 fun lists:foldl/3; ..571 rev -> ..572 fun lists:foldr/3 ..573 end, ..574 {ok, FoldResult} = KeyFoldFun( ..575 fun(Key, {ok, FoldAcc}) -> ..576 DocInfo = (catch couch_db:get_doc_info(Db, Key)), ..577 Doc = case DocInfo of ..578 {ok, #doc_info{id=Id, revs=[#rev_info{deleted=false, rev=Rev}|_]}} -> ..579 {{Id, Id}, {[{rev, couch_doc:rev_to_str(Rev)}]}}; ..580 {ok, #doc_info{id=Id, revs=[#rev_info{deleted=true, rev=Rev}|_]}} -> ..581 {{Id, Id}, {[{rev, couch_doc:rev_to_str(Rev)}, {deleted, true}]}}; ..582 not_found -> ..583 {{Key, error}, not_found}; ..584 _ -> ..585 ?LOG_ERROR("Invalid DocInfo: ~p", [DocInfo]), ..586 throw({error, invalid_doc_info}) ..587 end, ..588 Acc = (catch FoldlFun(Doc, 0, FoldAcc)), ..589 case Acc of ..590 {stop, Acc2} -> ..591 {ok, Acc2}; ..592 _ -> ..593 Acc ..594 end ..595 end, {ok, FoldAccInit}, Keys), ..596 couch_httpd_view:finish_view_fold(Req, TotalRowCount, {ok, FoldResult}) ..597 end ..598 end). ..599 ..600 db_doc_req(#httpd{method='DELETE'}=Req, Db, DocId) -> ..601 % check for the existence of the doc to handle the 404 case. ..602 couch_doc_open(Db, DocId, nil, []), ..603 case couch_httpd:qs_value(Req, "rev") of ..604 undefined -> ..605 update_doc(Req, Db, DocId, {[{<<"_deleted">>,true}]}); ..606 Rev -> ..607 update_doc(Req, Db, DocId, {[{<<"_rev">>, ?l2b(Rev)},{<<"_deleted">>,true}]}) ..608 end; ..609 ..610 db_doc_req(#httpd{method='GET'}=Req, Db, DocId) -> ..611 #doc_query_args{ ..612 show = Format, ..613 rev = Rev, ..614 open_revs = Revs, ..615 options = Options ..616 } = parse_doc_query(Req), ..617 case Format of ..618 nil -> ..619 case Revs of ..620 [] -> ..621 Doc = couch_doc_open(Db, DocId, Rev, Options), ..622 DiskEtag = couch_httpd:doc_etag(Doc), ..623 case Doc#doc.meta of ..624 [] -> ..625 % output etag only when we have no meta ..626 couch_httpd:etag_respond(Req, DiskEtag, fun() -> ..627 send_json(Req, 200, [{"Etag", DiskEtag}], couch_doc:to_json_obj(Doc, Options)) ..628 end); ..629 _ -> ..630 send_json(Req, 200, [], couch_doc:to_json_obj(Doc, Options)) ..631 end; ..632 _ -> ..633 {ok, Results} = couch_db:open_doc_revs(Db, DocId, Revs, Options), ..634 {ok, Resp} = start_json_response(Req, 200), ..635 send_chunk(Resp, "["), ..636 % We loop through the docs. The first time through the separator ..637 % is whitespace, then a comma on subsequent iterations. ..638 lists:foldl( ..639 fun(Result, AccSeparator) -> ..640 case Result of ..641 {ok, Doc} -> ..642 JsonDoc = couch_doc:to_json_obj(Doc, Options), ..643 Json = ?JSON_ENCODE({[{ok, JsonDoc}]}), ..644 send_chunk(Resp, AccSeparator ++ Json); ..645 {{not_found, missing}, RevId} -> ..646 Json = ?JSON_ENCODE({[{"missing", RevId}]}), ..647 send_chunk(Resp, AccSeparator ++ Json) ..648 end, ..649 "," % AccSeparator now has a comma ..650 end, ..651 "", Results), ..652 send_chunk(Resp, "]"), ..653 end_json_response(Resp) ..654 end; ..655 _ -> ..656 {DesignName, ShowName} = Format, ..657 couch_httpd_show:handle_doc_show(Req, DesignName, ShowName, DocId, Db) ..658 end; ..659 ..660 db_doc_req(#httpd{method='POST'}=Req, Db, DocId) -> ..661 couch_doc:validate_docid(DocId), ..662 case couch_httpd:header_value(Req, "content-type") of ..663 "multipart/form-data" ++ _Rest -> ..664 ok; ..665 _Else -> ..666 throw({bad_ctype, <<"Invalid Content-Type header for form upload">>}) ..667 end, ..668 Form = couch_httpd:parse_form(Req), ..669 Rev = couch_doc:parse_rev(list_to_binary(proplists:get_value("_rev", Form))), ..670 {ok, [{ok, Doc}]} = couch_db:open_doc_revs(Db, DocId, [Rev], []), ..671 ..672 UpdatedAtts = [ ..673 #att{name=validate_attachment_name(Name), ..674 type=list_to_binary(ContentType), ..675 data=Content} || ..676 {Name, {ContentType, _}, Content} <- ..677 proplists:get_all_values("_attachments", Form) ..678 ], ..679 #doc{atts=OldAtts} = Doc, ..680 OldAtts2 = lists:flatmap( ..681 fun(#att{name=OldName}=Att) -> ..682 case [1 || A <- UpdatedAtts, A#att.name == OldName] of ..683 [] -> [Att]; % the attachment wasn't in the UpdatedAtts, return it ..684 _ -> [] % the attachment was in the UpdatedAtts, drop it ..685 end ..686 end, OldAtts), ..687 NewDoc = Doc#doc{ ..688 atts = UpdatedAtts ++ OldAtts2 ..689 }, ..690 {ok, NewRev} = couch_db:update_doc(Db, NewDoc, []), ..691 ..692 send_json(Req, 201, [{"Etag", "\"" ++ ?b2l(couch_doc:rev_to_str(NewRev)) ++ "\""}], {[ ..693 {ok, true}, ..694 {id, DocId}, ..695 {rev, couch_doc:rev_to_str(NewRev)} ..696 ]}); ..697 ..698 db_doc_req(#httpd{method='PUT'}=Req, Db, DocId) -> ..699 couch_doc:validate_docid(DocId), ..700 Json = couch_httpd:json_body(Req), ..701 case couch_httpd:qs_value(Req, "batch") of ..702 "ok" -> ..703 % batch ..704 Doc = couch_doc_from_req(Req, DocId, Json), ..705 ok = couch_batch_save:eventually_save_doc(Db#db.name, Doc, Db#db.user_ctx), ..706 send_json(Req, 202, [], {[ ..707 {ok, true}, ..708 {id, DocId} ..709 ]}); ..710 _Normal -> ..711 % normal ..712 Location = absolute_uri(Req, "/" ++ ?b2l(Db#db.name) ++ "/" ++ ?b2l(DocId)), ..713 update_doc(Req, Db, DocId, Json, [{"Location", Location}]) ..714 end; ..715 ..716 db_doc_req(#httpd{method='COPY'}=Req, Db, SourceDocId) -> ..717 SourceRev = ..718 case extract_header_rev(Req, couch_httpd:qs_value(Req, "rev")) of ..719 missing_rev -> nil; ..720 Rev -> Rev ..721 end, ..722 {TargetDocId, TargetRevs} = parse_copy_destination_header(Req), ..723 % open old doc ..724 Doc = couch_doc_open(Db, SourceDocId, SourceRev, []), ..725 % save new doc ..726 {ok, NewTargetRev} = couch_db:update_doc(Db, ..727 Doc#doc{id=TargetDocId, revs=TargetRevs}, []), ..728 % respond ..729 send_json(Req, 201, ..730 [{"Etag", "\"" ++ ?b2l(couch_doc:rev_to_str(NewTargetRev)) ++ "\""}], ..731 update_doc_result_to_json(TargetDocId, {ok, NewTargetRev})); ..732 ..733 db_doc_req(Req, _Db, _DocId) -> ..734 send_method_not_allowed(Req, "DELETE,GET,HEAD,POST,PUT,COPY"). ..735 ..736 ..737 update_doc_result_to_json({{Id, Rev}, Error}) -> ..738 {_Code, Err, Msg} = couch_httpd:error_info(Error), ..739 {[{id, Id}, {rev, couch_doc:rev_to_str(Rev)}, ..740 {error, Err}, {reason, Msg}]}. ..741 ..742 update_doc_result_to_json(#doc{id=DocId}, Result) -> ..743 update_doc_result_to_json(DocId, Result); ..744 update_doc_result_to_json(DocId, {ok, NewRev}) -> ..745 {[{id, DocId}, {rev, couch_doc:rev_to_str(NewRev)}]}; ..746 update_doc_result_to_json(DocId, Error) -> ..747 {_Code, ErrorStr, Reason} = couch_httpd:error_info(Error), ..748 {[{id, DocId}, {error, ErrorStr}, {reason, Reason}]}. ..749 ..750 ..751 update_doc(Req, Db, DocId, Json) -> ..752 update_doc(Req, Db, DocId, Json, []). ..753 ..754 update_doc(Req, Db, DocId, Json, Headers) -> ..755 #doc{deleted=Deleted} = Doc = couch_doc_from_req(Req, DocId, Json), ..756 ..757 case couch_httpd:header_value(Req, "X-Couch-Full-Commit", "false") of ..758 "true" -> ..759 Options = [full_commit]; ..760 _ -> ..761 Options = [] ..762 end, ..763 {ok, NewRev} = couch_db:update_doc(Db, Doc, Options), ..764 NewRevStr = couch_doc:rev_to_str(NewRev), ..765 ResponseHeaders = [{"Etag", <<"\"", NewRevStr/binary, "\"">>}] ++ Headers, ..766 send_json(Req, if Deleted -> 200; true -> 201 end, ..767 ResponseHeaders, {[ ..768 {ok, true}, ..769 {id, DocId}, ..770 {rev, NewRevStr}]}). ..771 ..772 couch_doc_from_req(Req, DocId, Json) -> ..773 Doc = couch_doc:from_json_obj(Json), ..774 validate_attachment_names(Doc), ..775 ExplicitDocRev = ..776 case Doc#doc.revs of ..777 {Start,[RevId|_]} -> {Start, RevId}; ..778 _ -> undefined ..779 end, ..780 case extract_header_rev(Req, ExplicitDocRev) of ..781 missing_rev -> ..782 Revs = {0, []}; ..783 {Pos, Rev} -> ..784 Revs = {Pos, [Rev]} ..785 end, ..786 Doc#doc{id=DocId, revs=Revs}. ..787 ..788 ..789 % Useful for debugging ..790 % couch_doc_open(Db, DocId) -> ..791 % couch_doc_open(Db, DocId, nil, []). ..792 ..793 couch_doc_open(Db, DocId, Rev, Options) -> ..794 case Rev of ..795 nil -> % open most recent rev ..796 case couch_db:open_doc(Db, DocId, Options) of ..797 {ok, Doc} -> ..798 Doc; ..799 Error -> ..800 throw(Error) ..801 end; ..802 _ -> % open a specific rev (deletions come back as stubs) ..803 case couch_db:open_doc_revs(Db, DocId, [Rev], Options) of ..804 {ok, [{ok, Doc}]} -> ..805 Doc; ..806 {ok, [Else]} -> ..807 throw(Else) ..808 end ..809 end. ..810 ..811 % Attachment request handlers ..812 ..813 db_attachment_req(#httpd{method='GET'}=Req, Db, DocId, FileNameParts) -> ..814 FileName = list_to_binary(mochiweb_util:join(lists:map(fun binary_to_list/1, FileNameParts),"/")), ..815 #doc_query_args{ ..816 rev=Rev, ..817 options=Options ..818 } = parse_doc_query(Req), ..819 #doc{ ..820 atts=Atts ..821 } = Doc = couch_doc_open(Db, DocId, Rev, Options), ..822 case [A || A <- Atts, A#att.name == FileName] of ..823 [] -> ..824 throw({not_found, "Document is missing attachment"}); ..825 [#att{type=Type}=Att] -> ..826 Etag = couch_httpd:doc_etag(Doc), ..827 couch_httpd:etag_respond(Req, Etag, fun() -> ..828 {ok, Resp} = start_chunked_response(Req, 200, [ ..829 {"ETag", Etag}, ..830 {"Cache-Control", "must-revalidate"}, ..831 {"Content-Type", binary_to_list(Type)}%, ..832 % My understanding of http://www.faqs.org/rfcs/rfc2616.html ..833 % says that we should not use Content-Length with a chunked ..834 % encoding. Turning this off makes libcurl happy, but I am ..835 % open to discussion. (jchris) ..836 % ..837 % Can you point to the section that makes you think that? (jan) ..838 % {"Content-Length", integer_to_list(couch_doc:bin_size(Bin))} ..839 ]), ..840 couch_doc:att_foldl(Att, ..841 fun(BinSegment, _) -> send_chunk(Resp, BinSegment) end,[]), ..842 send_chunk(Resp, "") ..843 end) ..844 end; ..845 ..846 ..847 db_attachment_req(#httpd{method=Method}=Req, Db, DocId, FileNameParts) ..848 when (Method == 'PUT') or (Method == 'DELETE') -> ..849 FileName = validate_attachment_name( ..850 mochiweb_util:join( ..851 lists:map(fun binary_to_list/1, ..852 FileNameParts),"/")), ..853 ..854 NewAtt = case Method of ..855 'DELETE' -> ..856 []; ..857 _ -> ..858 [#att{ ..859 name=FileName, ..860 type = case couch_httpd:header_value(Req,"Content-Type") of ..861 undefined -> ..862 % We could throw an error here or guess by the FileName. ..863 % Currently, just giving it a default. ..864 <<"application/octet-stream">>; ..865 CType -> ..866 list_to_binary(CType) ..867 end, ..868 data = case couch_httpd:header_value(Req,"Content-Length") of ..869 undefined -> ..870 fun(MaxChunkSize, ChunkFun, InitState) -> ..871 couch_httpd:recv_chunked(Req, MaxChunkSize, ..872 ChunkFun, InitState) ..873 end; ..874 Length -> ..875 fun() -> couch_httpd:recv(Req, 0) end ..876 end, ..877 len = case couch_httpd:header_value(Req,"Content-Length") of ..878 undefined -> ..879 undefined; ..880 Length -> ..881 list_to_integer(Length) ..882 end ..883 }] ..884 end, ..885 ..886 Doc = case extract_header_rev(Req, couch_httpd:qs_value(Req, "rev")) of ..887 missing_rev -> % make the new doc ..888 #doc{id=DocId}; ..889 Rev -> ..890 case couch_db:open_doc_revs(Db, DocId, [Rev], []) of ..891 {ok, [{ok, Doc0}]} -> Doc0; ..892 {ok, [Error]} -> throw(Error) ..893 end ..894 end, ..895 ..896 #doc{atts=Atts} = Doc, ..897 DocEdited = Doc#doc{ ..898 atts = NewAtt ++ [A || A <- Atts, A#att.name /= FileName] ..899 }, ..900 {ok, UpdatedRev} = couch_db:update_doc(Db, DocEdited, []), ..901 #db{name=DbName} = Db, ..902 ..903 {Status, Headers} = case Method of ..904 'DELETE' -> ..905 {200, []}; ..906 _ -> ..907 {201, [{"Location", absolute_uri(Req, "/" ++ ..908 binary_to_list(DbName) ++ "/" ++ ..909 binary_to_list(DocId) ++ "/" ++ ..910 binary_to_list(FileName) ..911 )}]} ..912 end, ..913 send_json(Req,Status, Headers, {[ ..914 {ok, true}, ..915 {id, DocId}, ..916 {rev, couch_doc:rev_to_str(UpdatedRev)} ..917 ]}); ..918 ..919 db_attachment_req(Req, _Db, _DocId, _FileNameParts) -> ..920 send_method_not_allowed(Req, "DELETE,GET,HEAD,PUT"). ..921 ..922 parse_doc_format(FormatStr) when is_binary(FormatStr) -> ..923 parse_doc_format(?b2l(FormatStr)); ..924 parse_doc_format(FormatStr) when is_list(FormatStr) -> ..925 SplitFormat = lists:splitwith(fun($/) -> false; (_) -> true end, FormatStr), ..926 case SplitFormat of ..927 {DesignName, [$/ | ShowName]} -> {?l2b(DesignName), ?l2b(ShowName)}; ..928 _Else -> throw({bad_request, <<"Invalid doc format">>}) ..929 end; ..930 parse_doc_format(_BadFormatStr) -> ..931 throw({bad_request, <<"Invalid doc format">>}). ..932 ..933 parse_doc_query(Req) -> ..934 lists:foldl(fun({Key,Value}, Args) -> ..935 case {Key, Value} of ..936 {"attachments", "true"} -> ..937 Options = [attachments | Args#doc_query_args.options], ..938 Args#doc_query_args{options=Options}; ..939 {"meta", "true"} -> ..940 Options = [revs_info, conflicts, deleted_conflicts | Args#doc_query_args.options], ..941 Args#doc_query_args{options=Options}; ..942 {"revs", "true"} -> ..943 Options = [revs | Args#doc_query_args.options], ..944 Args#doc_query_args{options=Options}; ..945 {"local_seq", "true"} -> ..946 Options = [local_seq | Args#doc_query_args.options], ..947 Args#doc_query_args{options=Options}; ..948 {"revs_info", "true"} -> ..949 Options = [revs_info | Args#doc_query_args.options], ..950 Args#doc_query_args{options=Options}; ..951 {"conflicts", "true"} -> ..952 Options = [conflicts | Args#doc_query_args.options], ..953 Args#doc_query_args{options=Options}; ..954 {"deleted_conflicts", "true"} -> ..955 Options = [deleted_conflicts | Args#doc_query_args.options], ..956 Args#doc_query_args{options=Options}; ..957 {"rev", Rev} -> ..958 Args#doc_query_args{rev=couch_doc:parse_rev(Rev)}; ..959 {"open_revs", "all"} -> ..960 Args#doc_query_args{open_revs=all}; ..961 {"open_revs", RevsJsonStr} -> ..962 JsonArray = ?JSON_DECODE(RevsJsonStr), ..963 Args#doc_query_args{open_revs=[couch_doc:parse_rev(Rev) || Rev <- JsonArray]}; ..964 {"show", FormatStr} -> ..965 Args#doc_query_args{show=parse_doc_format(FormatStr)}; ..966 _Else -> % unknown key value pair, ignore. ..967 Args ..968 end ..969 end, #doc_query_args{}, couch_httpd:qs(Req)). ..970 ..971 ..972 extract_header_rev(Req, ExplicitRev) when is_binary(ExplicitRev) or is_list(ExplicitRev)-> ..973 extract_header_rev(Req, couch_doc:parse_rev(ExplicitRev)); ..974 extract_header_rev(Req, ExplicitRev) -> ..975 Etag = case couch_httpd:header_value(Req, "If-Match") of ..976 undefined -> undefined; ..977 Value -> couch_doc:parse_rev(string:strip(Value, both, $")) ..978 end, ..979 case {ExplicitRev, Etag} of ..980 {undefined, undefined} -> missing_rev; ..981 {_, undefined} -> ExplicitRev; ..982 {undefined, _} -> Etag; ..983 _ when ExplicitRev == Etag -> Etag; ..984 _ -> ..985 throw({bad_request, "Document rev and etag have different values"}) ..986 end. ..987 ..988 ..989 parse_copy_destination_header(Req) -> ..990 Destination = couch_httpd:header_value(Req, "Destination"), ..991 case regexp:match(Destination, "\\?") of ..992 nomatch -> ..993 {list_to_binary(Destination), {0, []}}; ..994 {match, _, _} -> ..995 {ok, [DocId, RevQueryOptions]} = regexp:split(Destination, "\\?"), ..996 {ok, [_RevQueryKey, Rev]} = regexp:split(RevQueryOptions, "="), ..997 {Pos, RevId} = couch_doc:parse_rev(Rev), ..998 {list_to_binary(DocId), {Pos, [RevId]}} ..999 end. .1000 .1001 validate_attachment_names(Doc) -> .1002 lists:foreach(fun(#att{name=Name}) -> .1003 validate_attachment_name(Name) .1004 end, Doc#doc.atts). .1005 .1006 validate_attachment_name(Name) when is_list(Name) -> .1007 validate_attachment_name(list_to_binary(Name)); .1008 validate_attachment_name(<<"_",_/binary>>) -> .1009 throw({bad_request, <<"Attachment name can't start with '_'">>}); .1010 validate_attachment_name(Name) -> .1011 case is_valid_utf8(Name) of .1012 true -> Name; .1013 false -> throw({bad_request, <<"Attachment name is not UTF-8 encoded">>}) .1014 end. .1015 .1016 %% borrowed from mochijson2:json_bin_is_safe() .1017 is_valid_utf8(<<>>) -> .1018 true; .1019 is_valid_utf8(<>) -> .1020 case C of .1021 $\" -> .1022 false; .1023 $\\ -> .1024 false; .1025 $\b -> .1026 false; .1027 $\f -> .1028 false; .1029 $\n -> .1030 false; .1031 $\r -> .1032 false; .1033 $\t -> .1034 false; .1035 C when C >= 0, C < $\s; C >= 16#7f, C =< 16#10FFFF -> .1036 false; .1037 C when C < 16#7f -> .1038 is_valid_utf8(Rest); .1039 _ -> .1040 false .1041 end.
Generated using etap 0.3.4.