Now that we can create contribution objects and convert them to and from JSON, the next step is to manage a list of contributions and make this list persistent in a JSON file.
We will achieve this with a service called se_contribution_list. Besides maintaining a persistent list of contribution objects, this service will have two more responsibilities:
Assign a unique identifier to each contribution object. The identifier will be an integer number starting at 1 and incremented by 1 for each new object.
Create contribution objects from the following 3 string values: first_name, last_name and random_number. This is an important feature because we will later build a web interface that enables users to submit their contribution. Data sent via HTTP are all provided as strings which have to be converted into typed objects. Moreover the users won't input an identifier or the date and time of their contribution. They only submit their first name, last name and random value for an arc.
se_contribution_list will have two public commands:
Command add_from_strings creates a contribution object from string values and then adds the object to the list of contributions saved in the JSON file
Command get_list reads all contributions from the JSON file and returns the complete list as an object.
The signature of command add_from_strings is defined as follows:
command add_from_strings
in first_name type:string end
in last_name type:string end
in random_number type:string end
in error_handler type:system_error_handler default:se_system_utilities.default_system_error_handler end
out result type:contribution voidable:yes end
out error type:system_error voidable:yes end
out_check check: i_result =r void xor i_error =r void end
script
end
endAs you can see, there is again an error_handler input argument and an error output argument because many things can go wrong. For example the input can be invalid or there is a file write error.
Command get_list can also fail and is therefore defined like this:
command get_list
in error_handler type:system_error_handler default:se_system_utilities.default_system_error_handler end
out result type:!indexed_list<contribution> voidable:yes end
out error type:system_error voidable:yes end
out_check check: i_result =r void xor i_error =r void end
script
end
endNote the type for output argument result: !indexed_list<contribution>. Here we use a generic type that denotes an indexed_list containing only contribution objects. The ! introduces the usage of a generic type, and the value between < and > defines the type of object stored in the list.
![]() | Note |
|---|---|
| For Java programmers: Although the syntax for generic types in Obix is similar to the syntax used in Java, generic types in Obix are conceptually different from those in Java. For example, in Obix there is no type erasure at runtime. This is important because type safety is always guaranteed and type parameter information is available at runtime. |
Before writing the code for se_contribution_list there is one feature we have to add to factory random_1_15 we created in a previous chapter. The underlying data type of our random value is an integer number. The value that will be provided through the web user interface, however, will be a string, because all data transmitted through HTTP requests are strings. Therefore we need a feature that converts a string object into a random_1_15 object. The best way to do this is to add a creator in factory random_1_15; a creator which takes a string as input and returns a random_1_15 object. Converting a string into an integer is again a very common feature needed in many applications. Therefore Obix's standard library provides a source code template that you can use, so that your code is reduced to a one-liner. Here is the template's source code, as defined in the standard library:
template integer32_create_from_string_creator
param type_suffix end
creator create_from_string
in string type:string end
%system_error_handler_input_argument
%result_xor_string_to_object_conversion_error_output < {type_suffix} >
script
var integer32_value value
fa_integer32_value.create_from_string_value ( &
value = i_string.value &
error_handler = i_error_handler ) &
( v_value = result &
o_error = error )
if o_error #r void then
o_result = void
exit script
end if
const attribute_check_error attribute_check_error = ty_{type_suffix}.value.check ( v_value )
if c_attribute_check_error #r void then
o_result = void
o_error = fa_string_to_object_conversion_error.create ( &
id = "string_to_{type_suffix}_conversion_error" &
description = c_attribute_check_error.description &
java_exception = void &
data = i_string &
type_of_result = "{type_suffix}" &
error_position = void )
i_error_handler.handle_system_error ( o_error )
exit script
end if
o_result = create ( v_value )
end
end
end templateTo add creator create_from_string in factory random_1_15:
Edit file fa_random_1_15.osc and add creator create_from_string by using the template. The full code should look as follows:
factory random_1_15 type:random_1_15 creator create kind:in_all end %integer32_create_from_string_creator < type_suffix:random_1_15 > end
Now we are ready to write se_contribution_list. The full code with comments and a global test script at the end is shown below. To create the service:
Create file se_contribution_list.osc in directory li_contribution/
Copy-paste the following code, then save the file and run compile_and_build and run_tests
service contribution_list
// The following two attributes define the files used to store data
// file 'contributions.json' contains the contribution objects in JSON format
// file 'next_contribution_id.txt' contains the next identifier for a new contribution object
// (integer starting at 1 and incremented by 1 each time )
attribute_list type:file private:yes kind:variable
attribute contributions_file default:fa_relative_file.create ( "contributions.json"~ ).make_absolute_file end
attribute next_identifier_file default:fa_relative_file.create ( "next_contribution_id.txt"~ ).make_absolute_file end
end
command add_from_strings
in first_name type:string end
in last_name type:string end
in random_number type:string end
%system_error_handler_input_argument
%result_xor_system_error_output < contribution >
script
// check 'i_first_name' by calling command 'ty_user_name.value.check'
const attribute_check_error first_name_error = ty_user_name.value.check ( i_first_name.value )
if c_first_name_error #r void then
o_error = fa_invalid_data_error.create ( &
description = c_first_name_error.description &
data = i_first_name )
i_error_handler.handle_system_error ( o_error )
exit script
end if
// check 'i_last_name'
const attribute_check_error last_name_error = ty_user_name.value.check ( i_last_name.value )
if c_last_name_error #r void then
o_error = fa_invalid_data_error.create ( &
description = c_last_name_error.description &
data = i_last_name )
i_error_handler.handle_system_error ( o_error )
exit script
end if
// convert 'i_random_number' provided as type 'string' to type 'random_1_15'
var random_1_15 typed_random_number
fa_random_1_15.create_from_string ( &
string = i_random_number &
error_handler = i_error_handler ) &
( v_typed_random_number = result &
o_error = error )
if o_error #r void then
exit script
end if
// define the identifier for the new object
// the service command 'se_text_file_IO.get_next_counter_from_text_file' in Obix's standard library
// is a utility that increments a counter stored in a text file
var positive32 identifier
se_text_file_IO.get_next_counter_from_text_file ( &
file = a_next_identifier_file &
error_handler = i_error_handler ) &
( v_identifier = result &
o_error = error )
if o_error #r void then
exit script
end if
// create the 'contribution' object
o_result = fa_contribution.create ( &
identifier = v_identifier &
first_name = fa_user_name.create ( i_first_name.value ) &
last_name = fa_user_name.create ( i_last_name.value ) &
random_number = v_typed_random_number &
date_time = se_date_time.current_local_date_time )
// append the 'contribution' object to file 'a_contributions_file'
o_error = append_contribution_to_file ( &
contribution = o_result &
file = a_contributions_file &
error_handler = i_error_handler )
if o_error #r void then
o_result = void
end if
end
end
// private command to store a 'contribution' object into a JSON file
command append_contribution_to_file private:yes
in contribution type:contribution end
in file type:file end
%system_error_handler_input_argument
%resource_error_output_argument
script
// if the file doesn't exist yet then create it
if not i_file.exists then
o_error = i_file.create_physically ( i_error_handler )
if o_error #r void then
exit script
end if
end if
// append the JSON representation of the 'i_contribution' object to file 'i_file'
o_error = se_text_file_IO.append_string_to_file ( &
string = i_contribution.to_JSON & se_string_constants.current_OS_new_line &
file = i_file &
error_handler = i_error_handler )
end
end
command get_list
%system_error_handler_input_argument
%result_xor_system_error_output < !indexed_list<contribution> >
script
// report an error if 'a_contributions_file' file doesn't exists
if not a_contributions_file.exists then
o_error = fa_system_error.create ( description = """File {{a_contributions_file}} doesn't exist.""" )
i_error_handler.handle_system_error ( o_error )
exit script
end if
// read all lines from file 'a_contributions_file'
var ty_indexed_string_list text_lines
se_text_file_IO.restore_lines_from_text_file ( &
file = a_contributions_file &
error_handler = i_error_handler ) &
( v_text_lines = result &
o_error = error )
if o_error #r void then
exit script
end if
// create a mutable list of 'contribution' objects
const !mutable_indexed_list<contribution> contribution_list = !mutable_indexed_list_factory<contribution>.create
// add all 'contribution' objects stored in the file to the mutable list
repeat for each string line in text_lines
// skip empty lines
if line.is_empty then
next repeat
end if
var contribution contribution
fa_contribution.create_from_JSON ( &
JSON = line &
error_handler = i_error_handler ) &
( v_contribution = result &
o_error = error )
if o_error #r void then
exit script
end if
c_contribution_list.append ( v_contribution )
end
// convert the mutable list into an immutable list and return the immutable list
o_result = c_contribution_list.to_immutable
end
end
test
script
// create temporary files for test purposes
a_contributions_file = se_file.create_temporary_file.result ( delete_file_on_exit = yes )
a_next_identifier_file = se_file.create_temporary_file.result ( delete_file_on_exit = yes )
// first test with valid data
var contribution result
var system_error error
add_from_strings ( first_name = "Joshua" &
last_name = "Gafter" &
random_number = "5" ) &
( v_result = result &
v_error = error )
verify v_error =r void
verify v_result.identifier =v 1
verify v_result.first_name.value =v "Joshua"~
verify v_result.last_name.value =v "Gafter"~
verify v_result.random_number.value =v 5~
// retain the result for later use
const contribution result1 = v_result
// check the file content
var string file_content = se_text_file_IO.restore_string_from_file.result ( file = a_contributions_file )
verify v_file_content =v v_result.to_JSON & se_string_constants.current_OS_new_line
// read data from file into a list
var !indexed_list<contribution> contribution_list = get_list.result
verify v_contribution_list.item_count =v 1
const contribution first_item = v_contribution_list.item_iterator.next
verify c_first_item.identifier =v 1
verify c_first_item.first_name.value =v "Joshua"~
verify c_first_item.last_name.value =v "Gafter"~
verify c_first_item.random_number.value =v 5~
// create a second object
add_from_strings ( first_name = "Albert" &
last_name = "Newton" &
random_number = "4" ) &
( v_result = result &
v_error = error )
verify v_error =r void
verify v_result.identifier =v 2
verify v_result.first_name.value =v "Albert"~
verify v_result.last_name.value =v "Newton"~
verify v_result.random_number.value =v 4~
// check the file content again
v_file_content = se_text_file_IO.restore_string_from_file.result ( file = a_contributions_file )
verify v_file_content =v c_result1.to_JSON & se_string_constants.current_OS_new_line & v_result.to_JSON & se_string_constants.current_OS_new_line
// read data from file into a list
v_contribution_list = get_list.result
verify v_contribution_list.item_count =v 2
// check if an error is reported in case of invalid data
add_from_strings ( first_name = "Albert<>" &
last_name = "Newton" &
random_number = "4" ) &
( v_result = result &
v_error = error )
verify v_result =r void
verify v_error #r void
// read data from file into a list
v_contribution_list = get_list.result
verify v_contribution_list.item_count =v 2
end
end
end service
![]() | Note |
|---|---|
It is interesting to note that the above In the above script we carefully check each value for its validity and we manage each possible error individually. This is the preferred way to write code because we have fine-grained control over every possible invalid data and we can provide explicit error messages and error handling. Instead of checking each potential error programmatically, however, we could also rely on the fact that a program error (also called an exception) occurs immediately whenever we try to store invalid data in an object. For example, if we created a Program errors can be programmatically catched, in the same way that exceptions can be catched in other programming languages. The following alternative code for command command add_from_strings
in first_name type:string end
in last_name type:string end
in random_number type:string end
out result type:contribution voidable:yes end
script
// if a program error (an exception) occurs within the following section then
// - the program error is stored in the implicitly defined variable 'v_program_error_'
// - script execution continues after the 'end section' instruction
section add_contribution on_error:continue
// create the 'contribution' object
// if data is invalid then
// - a program error (exception) will occur
// - the error will be stored in variable 'v_program_error_'
// - script execution will continue after the 'end section' instruction
o_result = fa_contribution.create ( &
identifier = se_text_file_IO.get_next_counter_from_text_file.result ( file = a_next_identifier_file ) &
first_name = fa_user_name.create ( i_first_name.value ) &
last_name = fa_user_name.create ( i_last_name.value ) &
random_number = fa_random_1_15.create_from_string.result ( string = i_random_number ) &
date_time = se_date_time.current_local_date_time )
// create file 'a_contributions_file' if it doesn't exists yet
if not a_contributions_file.exists then
a_contributions_file.create_physically
end if
// append the JSON representation of the 'contribution' object to file 'a_contributions_file'
// if an error occurs then return 'void' as result
if se_text_file_IO.append_string_to_file ( &
string = o_result.to_JSON & se_string_constants.current_OS_new_line &
file = a_contributions_file ) #r void then
o_result = void
end if
end section
// check if anything went wrong
if v_program_error_ #r void then
o_result = void
system.err.write_line ( string = "Contribution object could not be stored to file" )
end if
end
end |