PageRenderTime 38ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/rdbms/src/rdbms_mailmerge.erl

https://github.com/gebi/jungerl
Erlang | 487 lines | 230 code | 75 blank | 182 comment | 6 complexity | 3e13c7d9c7767c27853fd83ec89061ac MD5 | raw file
Possible License(s): LGPL-2.1, BSD-3-Clause, AGPL-1.0
  1. %%%
  2. %%% The contents of this file are subject to the Erlang Public License,
  3. %%% Version 1.0, (the "License"); you may not use this file except in
  4. %%% compliance with the License. You may obtain a copy of the License at
  5. %%% http://www.erlang.org/license/EPL1_0.txt
  6. %%%
  7. %%% Software distributed under the License is distributed on an "AS IS"
  8. %%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
  9. %%% the License for the specific language governing rights and limitations
  10. %%% under the License.
  11. %%%
  12. %%% The Original Code is rdbms_mailmerge-1.0.
  13. %%%
  14. %%% The Initial Developer of the Original Code is Ericsson Telecom
  15. %%% AB. Portions created by Ericsson are Copyright (C), 1998, Ericsson
  16. %%% Telecom AB. All Rights Reserved.
  17. %%%
  18. %%% Contributor(s): ______________________________________.
  19. %%%----------------------------------------------------------------------
  20. %%% #0. BASIC INFORMATION
  21. %%% ----------------------------------------------------------
  22. %%% %CCaseFile: rdbms_mailmerge.erl %
  23. %%% Author: Ulf Wiger <ulf.wiger@ericsson.com>
  24. %%% Description: Imports/exports a tab-delimited ASCII file.
  25. %%%
  26. %%% Modules used: lists, file, string, rdbms_import_server, rdbms
  27. %%% ----------------------------------------------------------
  28. -module(rdbms_mailmerge).
  29. -vsn('1.0').
  30. -date('99-01-08').
  31. -author('ulf.wiger@ericsson.com').
  32. %%% #2. EXPORT LISTS
  33. %%% ----------------------------------------------------------
  34. %%% #2.1 EXPORTED INTERFACE FUNCTIONS
  35. %%% ----------------------------------------------------------
  36. -export([file/1, files/1, dir/1]).
  37. -export([export_data/2]).
  38. %%% ----------------------------------------------------------
  39. %%% #2.2 EXPORTED INTERNAL FUNCTIONS
  40. %%% ----------------------------------------------------------
  41. -include_lib("kernel/include/file.hrl").
  42. % Default delimiters
  43. -define(ROW_DELIM, 10). % newline
  44. -define(COL_DELIM, 9). % tab
  45. -define(REC_DELIM, $.). % dot, e.g. "person.name"
  46. -define(MULT_DELIM, $,). % comma, e.g. "person.office, office.name"
  47. -define(BLOCK_DELIM, $.). % A single dot on one line separates blocks.
  48. %%% #3. CODE
  49. %%% #---------------------------------------------------------
  50. %%% #3.1 CODE FOR THE EXPORTED INTERFACE FUNCTIONS
  51. %%% #---------------------------------------------------------
  52. %%% ----------------------------------------------------------
  53. %%% #3.1.1 file(File_name)
  54. %%% Input: File_name = a string identifying the import file
  55. %%% Output: ok | {error, Reason}.
  56. %%% Exceptions: Result = {error, Reason} if
  57. %%% - Records in header do not exist in dictionary
  58. %%% - labels in header do not match record elements
  59. %%% - input data violates integrity constraints
  60. %%% Description: rdbms_import_mailmerge:file/1 imports a tab-delimited
  61. %%% file into Mnesia. Each file is imported atomically as
  62. %%% one transaction, i.e. either all rows or none are
  63. %%% imported.
  64. %%% A file can contain multiple blocks of data,
  65. %%% separated by a line containing only a period.
  66. %%% Each block must begin with a header line
  67. %%% containing the column names, which will be matched
  68. %%% against the definition of the given record.
  69. %%% ----------------------------------------------------------
  70. file(File) ->
  71. {ok, Pid} = rdbms_import_server:start_link(),
  72. case file(File, Pid) of
  73. ok ->
  74. case rdbms_import_server:commit(Pid) of
  75. {aborted, Reason} ->
  76. {error, Reason};
  77. ok ->
  78. ok
  79. end;
  80. {error, Reason} ->
  81. rdbms_import_server:abort(Pid),
  82. {error, Reason}
  83. end.
  84. file(File, Pid) ->
  85. case file:open(File, [read]) of
  86. {ok, Fd} ->
  87. case read_file(Fd, Pid) of
  88. ok ->
  89. file:close(Fd),
  90. ok;
  91. Other ->
  92. file:close(Fd),
  93. {error, Other}
  94. end;
  95. {error, Reason} ->
  96. error_logger:format("Error reading file ~p.~n", [File]),
  97. {error, Reason}
  98. end.
  99. %%% ----------------------------------------------------------
  100. %%% #3.1.2 files([string()])
  101. %%% Input: A list of file names
  102. %%% Output: A list of {File, Result} tuples.
  103. %%% Exceptions: Same as file/1.
  104. %%% Description: files/1 calls file/1 for each file in the
  105. %%% list. Each file is imported atomically, and
  106. %%% a summary is returned to the calling function.
  107. %%% ----------------------------------------------------------
  108. files([File|T]) ->
  109. Res = file(File),
  110. [{File, Res}|files(T)];
  111. files([]) ->
  112. [].
  113. %%% ----------------------------------------------------------
  114. %%% #3.1.3 dir(string())
  115. %%% Input: A directory name
  116. %%% Output: A list of {File, Result} tuples.
  117. %%% Exceptions: Same as file/1.
  118. %%% Description: dir/1 calls files/1 with all files found
  119. %%% in the given directory.
  120. %%% ----------------------------------------------------------
  121. dir(Directory) ->
  122. case file:file_info(Directory) of
  123. {ok, {_, directory, _, _, _, _, _}} ->
  124. {ok, FileList} = file:list_dir(Directory),
  125. files([Directory ++ "/" ++ F || F <- FileList]);
  126. Other ->
  127. error_logger:format("Error reading directory ~p.~n", [Directory]),
  128. {error, dir}
  129. end.
  130. %%% ----------------------------------------------------------
  131. %%% #3.1.2 export_data(Dir : string(), Tabs : all | [atom()])
  132. %%% Input: DirectoryName, ListOfTableNames
  133. %%% Output: {atomic, [ok|...]}
  134. %%% Exceptions: {aborted, Reason} if anything goes wrong
  135. %%% or 'EXIT' if Dir couldn't be verified/created
  136. %%% Description: Exports the given mnesia tables to text files in Dir.
  137. %%% The text files have the same format as the import
  138. %%% data files, and are named TabName.txt.
  139. %%% This function takes a read lock on the given tables
  140. %%% before exporting the data. If Tabs == all, all tables
  141. %%% except the mnesia schema are exported.
  142. %%% ----------------------------------------------------------
  143. export_data(Dir, Tabs) ->
  144. verify_dir(Dir),
  145. {ok, CurDir} = file:get_cwd(),
  146. ok = file:set_cwd(Dir),
  147. Res = export_data(Tabs),
  148. ok = file:set_cwd(CurDir),
  149. Res.
  150. %%% #---------------------------------------------------------
  151. %%% #3.2 CODE FOR EXPORTED INTERNAL FUNCTIONS
  152. %%% #---------------------------------------------------------
  153. %%% none
  154. %%% #---------------------------------------------------------
  155. %%% #3.3 CODE FOR INTERNAL FUNCTIONS
  156. %%% #---------------------------------------------------------
  157. export_data(all) ->
  158. Tabs = mnesia:system_info(tables) -- [schema],
  159. do_export_data(Tabs);
  160. export_data(Tabs) when list(Tabs) ->
  161. do_export_data(Tabs).
  162. do_export_data(Tabs) ->
  163. mnesia:transaction(fun() ->
  164. [mnesia:read_lock_table(T) || T <- Tabs],
  165. [export_tab(T) || T <- Tabs]
  166. end).
  167. export_tab(Tab) ->
  168. Attrs = mnesia:table_info(Tab, attributes),
  169. WildP = mnesia:table_info(Tab, wild_pattern),
  170. TabStr = atom_to_list(Tab),
  171. {ok, Fd} = file:open(TabStr ++ ".txt", [write]),
  172. export_header(Fd, TabStr, Attrs),
  173. export_data(Fd, Tab, mnesia:dirty_slot(Tab, 0), 0),
  174. file:close(Fd).
  175. export_header(Fd, TabStr, Attrs) ->
  176. Str = header_string(TabStr, Attrs),
  177. ok = io:format(Fd, "~s~n", [Str]).
  178. export_data(Fd, Tab, '$end_of_table', Slot) -> ok;
  179. export_data(Fd, Tab, Objs, Slot) ->
  180. export_objects(Fd, Objs),
  181. Slot1 = Slot+1,
  182. export_data(Fd, Tab, mnesia:dirty_slot(Tab, Slot1), Slot1).
  183. export_objects(Fd, [Obj|Objs]) ->
  184. [_|Flds] = tuple_to_list(Obj),
  185. Str = data_string(Flds),
  186. ok = io:format(Fd, "~s~n", [Str]),
  187. export_objects(Fd, Objs);
  188. export_objects(Fd, []) -> ok.
  189. data_string([Term]) ->
  190. io_lib:format("~w", [Term]);
  191. data_string([H|T]) ->
  192. io_lib:format("~w\t", [H]) ++ data_string(T).
  193. %% mnesia enforces the rule that each tab must have at least one
  194. %% non-key attribute. Thus, the following code will work.
  195. header_string(TabStr, [Attr]) ->
  196. TabStr ++
  197. ("." ++ atom_to_list(Attr));
  198. header_string(TabStr, [A|Attrs]) ->
  199. TabStr ++
  200. ("." ++
  201. (atom_to_list(A) ++
  202. ("\t" ++ header_string(TabStr, Attrs))));
  203. header_string(_, []) ->
  204. exit(bad_attributes).
  205. %%% ----------------------------------------------------------
  206. %%% #3.3.1 store_blocks(Binary, File_name)
  207. %%% Input: File content, file name
  208. %%% Output: ok | {error, Reason}.
  209. %%% Exceptions: None, provided correct input values.
  210. %%% Description: store_blocks/2 tries to interpret the
  211. %%% binary object Bin as a bunch of data to
  212. %%% import into Mnesia. If data is correct, it
  213. %%% inserts it into Mnesia within a transaction.
  214. %%% File_name is only used in the return value.
  215. %%% ----------------------------------------------------------
  216. read_file(Fd, Pid) ->
  217. read_block(Fd, Pid).
  218. read_block(Fd, Pid) ->
  219. case io:get_line(Fd, []) of
  220. eof ->
  221. ok;
  222. ".\n" ->
  223. read_block(Fd, Pid);
  224. [$%|_] ->
  225. read_block(Fd, Pid);
  226. Str ->
  227. {[B], []} = rows(Str, []),
  228. {ok, Hdr}= detailed_columns(B),
  229. rdbms_import_server:header(Pid, Hdr),
  230. read_block_data(Fd, Pid)
  231. end.
  232. read_block_data(Fd, Pid) ->
  233. case io:get_line(Fd, []) of
  234. eof ->
  235. ok;
  236. ".\n" ->
  237. read_block(Fd, Pid);
  238. [$%|_] ->
  239. read_block_data(Fd, Pid);
  240. Str ->
  241. S = remove_eol(Str),
  242. case columns(S) of
  243. [] ->
  244. read_block_data(Fd, Pid);
  245. L ->
  246. rdbms_import_server:data(Pid, list_to_tuple(L)),
  247. read_block_data(Fd, Pid)
  248. end
  249. end.
  250. remove_eol([]) ->
  251. [];
  252. remove_eol(Str) ->
  253. case lists:reverse(Str) of
  254. [?ROW_DELIM|Rest] ->
  255. lists:reverse(Rest);
  256. S ->
  257. Str
  258. end.
  259. %%% ----------------------------------------------------------
  260. %%% #3.3.5 rows(Bytes), read_to_eol/2
  261. %%% Input: A list of bytes
  262. %%% Output: {Block_rows, Rest}.
  263. %%% Exceptions: No error checking.
  264. %%% Description: rows/2 is a variant of string:tokens, which
  265. %%% splits one block (ended by ?BLOCK_DELIM or eof)
  266. %%% into rows (separated by ?ROW_DELIM,
  267. %%% ignoring empty lines.
  268. %%% ----------------------------------------------------------
  269. rows([?BLOCK_DELIM,?ROW_DELIM|Rest], Rows) ->
  270. {lists:reverse(Rows), Rest};
  271. rows([?ROW_DELIM|Rest], Rows) -> %ignore empty lines
  272. rows(Rest, Rows);
  273. rows([C|Bytes], Rows) ->
  274. {Row, Rest} = read_to_eol(Bytes, [C]),
  275. rows(Rest, [Row|Rows]);
  276. rows([], Rows) ->
  277. {lists:reverse(Rows), []}.
  278. read_to_eol([?ROW_DELIM|Rest], Line) ->
  279. {lists:reverse(Line), Rest};
  280. read_to_eol([C|Rest], Line) ->
  281. read_to_eol(Rest, [C|Line]);
  282. read_to_eol([], Line) ->
  283. {lists:reverse(Line), []}.
  284. %%% ----------------------------------------------------------
  285. %%% #3.3.7 columns(Row)
  286. %%% Input: A string signifying a row of data
  287. %%% Output: A list of strings, one for each column.
  288. %%% Exceptions: -
  289. %%% Description: This function divides a string into a list
  290. %%% of strings, based on column separators.
  291. %%% ----------------------------------------------------------
  292. columns(Row) ->
  293. tokens(Row).
  294. %%% ----------------------------------------------------------
  295. %%% #3.3.8 detailed_columns(Bytes)
  296. %%% Input: A list of bytes representing one row of data
  297. %%% Output: {ok ,Cols} Cols = A list of strings,
  298. %%% one for each column.
  299. %%% Exceptions: No error checking.
  300. %%% Description: The function uses as column delimiter whatever is
  301. %%% defined as ?COL_DELIM.
  302. %%% NOTE: This function uses a variant of string:tokens
  303. %%% rewritten to accept empty columns
  304. %%% detailed_columns/1 is used on the header row.
  305. %%% header columns are checked for validity and joins.
  306. %%% ----------------------------------------------------------
  307. detailed_columns(Row) ->
  308. Cols1 = tokens(Row, ""),
  309. Cols2 = multiples(Cols1),
  310. Cols3 = names(Cols2),
  311. {ok, Cols3}.
  312. %%% ----------------------------------------------------------
  313. %%% #3.3.9 tokens(Bytes), tokens1/3, tokens2/4
  314. %%% Input: A list of bytes representing one row of data
  315. %%% Output: A list of strings, one for each column.
  316. %%% Exceptions: No error checking.
  317. %%% Description: scan a string and return a list of columns,
  318. %%% as separated by ?COL_DELIM (e.g. tab).
  319. %%% Several delimiters in a row are to
  320. %%% be interpreted as empty columns (a default value
  321. %%% is inserted) and a trailing tab means a last empty
  322. %%% column.
  323. %%% tokens(String) defaults to tokens(String, NULL), i.e.
  324. %%% default value inserted is NULL.
  325. %%% ----------------------------------------------------------
  326. tokens(S) ->
  327. tokens(S, rdbms:null_value()).
  328. tokens(S, Null) ->
  329. tokens1(S, [], Null).
  330. tokens1([$ |S], Toks, Null) -> % ignore spaces.
  331. tokens1(S, Toks, Null);
  332. tokens1([?COL_DELIM|S], Toks, Null) ->
  333. tokens1(S, [Null|Toks], Null);
  334. tokens1([C|S], Toks, Null) ->
  335. tokens2(S, Toks, [C], Null);
  336. tokens1([], Toks, Null) ->
  337. lists:reverse([Null|Toks]).
  338. tokens2([?COL_DELIM|S], Toks, Cs, Null) ->
  339. tokens1(S, [lists:reverse(Cs)|Toks], Null);
  340. tokens2([C|S], Toks, Cs, Null) ->
  341. tokens2(S, Toks, [C|Cs], Null);
  342. tokens2([], Toks, Cs, Null) ->
  343. lists:reverse([lists:reverse(Cs)|Toks]).
  344. %%% ----------------------------------------------------------
  345. %%% #3.3.10 multiples(Columns)
  346. %%% Input: A list of header columns
  347. %%% Output: [ [Attr_name] ] a list of lists of attribute names.
  348. %%% Exceptions: No error checking.
  349. %%% Description: multiples/1 uses string:tokens to separate attr
  350. %%% names separated by ?MULT_DELIM (e.g. ',').
  351. %%% ----------------------------------------------------------
  352. multiples([Col|T]) ->
  353. [string:tokens(Col, [?MULT_DELIM, $ ])|multiples(T)];
  354. multiples([]) ->
  355. [].
  356. %%% ----------------------------------------------------------
  357. %%% #3.3.11 names(Columns), names1(Column)
  358. %%% Input: A list of Columns
  359. %%% Output: [ [{Rec_name, Attr_name}] ].
  360. %%% Exceptions: Throws all errors.
  361. %%% Description: names/1 parses all attr names using ?REC_DELIM
  362. %%% (e.g. '.')..
  363. %%% ----------------------------------------------------------
  364. names([Col|T]) ->
  365. [names1(Col)|names(T)];
  366. names([]) ->
  367. [].
  368. names1([N|T]) ->
  369. case string:tokens(N, [?REC_DELIM]) of
  370. [A,B] ->
  371. [{list_to_atom(A),list_to_atom(B)}|names1(T)];
  372. Other ->
  373. throw({error, {parse_error, Other}})
  374. end;
  375. names1([]) ->
  376. [].
  377. %%%----------------------------------------------------------------------
  378. %%% -type verify_dir(Dir : string())->
  379. %%% ok.
  380. %%% Input: Dir
  381. %%% Output: ok
  382. %%% Exceptions:
  383. %%% Description: This function verifies a directory name. If the directory
  384. %%% does not exist, this function tries to create it, and
  385. %%% any parent directories that may be missing.
  386. %%% The function exits if:
  387. %%% - the directory is read-only {'EXIT', {access, Dir}}
  388. %%% - the dir (or parent dir) could not be created
  389. %%% {'EXIT', {create, Dir}}
  390. %%%----------------------------------------------------------------------
  391. verify_dir(Dir) ->
  392. case file:read_file_info(Dir) of
  393. {ok, FI} when FI#file_info.type == directory,
  394. FI#file_info.access == read_write ->
  395. ok;
  396. {ok, FI} when FI#file_info.type == directory ->
  397. exit({access, Dir});
  398. {error, enoent} ->
  399. try_create(Dir)
  400. end.
  401. try_create("/") ->
  402. exit({create, "/"});
  403. try_create(".") ->
  404. exit({create, "/"});
  405. try_create(Dir) ->
  406. case file:make_dir(Dir) of
  407. {error, Reason} ->
  408. try_create(filename:dirname(Dir)),
  409. try_create(Dir);
  410. ok ->
  411. ok
  412. end.