Perl Modul LoxBerry::JSON

The class LoxBerry::JSON is intended to simplify working with json configuration files. It abstracts reading, writing and change recognition, and presents the content of the json file as an object variable. It supports simple key/value pairs, and also json objects and arrays. 

The advanced features allow to search for values in arrays and hashes.

LoxBerry Compatibility

This object class is in available from V1.4.0. Set the minimum LoxBerry version in your plugin.cfg.

Inclusions

This library automatically includes:

  • use JSON;

Abstract

use LoxBerry::JSON;
my $cfgfile = $lbpplugindir."/pluginconfig.json";
 
$jsonobj = LoxBerry::JSON->new();
$cfg = $jsonobj->open(filename => $cfgfile);
 
# Read values
print "Firstname: " . $cfg->{Firstname};
print "Surname: " . $cfg->{Surname};
 
# Change values
$cfg->{Firstname} = "Christian";
$cfg->{Surname} = "Fenzl";
 
# Write 
$jsonobj->write();
# On every write, the class checks for changes and omits writing if nothing has changed
$saved = $jsonobj->write();
# From LB2.2: write() returns 1 if content actually was changed/written, or undef if unchanged.
 
## File Locking and Exclusive Lock ## (from LB 2.2.1)
$jsonobj = LoxBerry::JSON->new();
# Normal Shared Lock, with timeout 10 sec.
$cfg = $jsonobj->open(filename => $cfgfile, locktimeout => 10);
# Exclusive Lock, no timeout
$cfg = $jsonobj->open(filename => $cfgfile, lockexclusive => 1);
# Exclusive Lock, with timeout 10 sec.
$cfg = $jsonobj->open(filename => $cfgfile, lockexclusive => 1, locktimeout => 10);
 
## Specials ##
 
# Parse an already existing json string (available from LB 1.4.1)
my $jsonstring = ' { "Firstname": "christian", "Surname": "fenzl" } ';
$cfg = $jsonobj->parse($jsonstring);
 
# Return a flat key=value hash from a hierarchical data structure (available from LB 2.0)
my $flat_hashref = $jsonobj->flatten();
my $flat_hashref = $jsonobj->flatten($prefix);
 
# Use $jsonobj in HTML::Template associate (available from LB 2.0)
my $template = HTML::Template->new(
    filename  => 'json_param.tmpl',
    associate => $jsonobj,
);
 
# Encode current data hash to json string, optional pretty printing (available from LB 2.0)
my $jsonstr = $jsonobj->encode();
my $jsonstr = $jsonobj->encode( pretty => 1 );
 
# Generate ready and escaped JavaScript code from current data (available from LB 2.0)
print $jsonobj->jsblock('jsvariablename');
# Returns: cfg = JSON.parse('<your data as json correctly escaped>');
 
# Enable debugging and dumping
$LoxBerry::JSON::DEBUG = 1; # Enables debug messages
$LoxBerry::JSON::DUMP = 1;  # Enables dumping of the current dataset
 
# Keep the file readonly in every case
$cfg = $jsonobj->open(filename => $cfgfile, readonly => 1);
 
# Write automatically on close
$cfg = $jsonobj->open(filename => $cfgfile, writeonclose => 1);
 
# Returns the filename
my $filename = $jsonobj->filename();
# Changes the filename (available from LB 1.4.1)
$jsonobj->filename('/tmp/newfilename.json');
 
# Find
my @result = $jsonobj->find($cfg->{namesarray}, "$_->{Name} eq 'Christian'");
# See the detailed description for use-cases
 
# Dump
$jsonobj->dump{$cfg->{namesarray});

Parameter

Parameter Optional Default Description
filename JSON file, that should be opened. If the file doesn't exist, it will be created on write.
writeonclose x 0 If 0, you need to explicitely call write. write can also be called multiple times. If 1, the file is written automatically on descruction of the object.
readonly x 0 If 1, every writing to the file is omitted.

Basically, the function always compares the initial content with the new content, and only writes, if the content has changed. This also happens when write is called explicitely.

With writeonclose ⇒ 1 is not written immediately on change of a value, but if the descructor is called, that means

  • on leaving the scope of the $jsonparser object (thereforce, define $jsonparser in the main section, if you want to use it globally)
  • on exit of the program.

Open and change JSON - Example

#!/usr/bin/perl
use LoxBerry::System;
use LoxBerry::JSON;
 
$LoxBerry::JSON::DEBUG = 1;
$LoxBerry::JSON::DUMP = 1;
 
my $jsonparser = LoxBerry::JSON->new();
my $config = $jsonparser->open(filename => "/tmp/somefile.json", writeonclose => 0);
 
# Error handling
if (!$config) {
    print "Error loading file\n";
} else {
    print "File loaded\n";
}
 
print "Version of the file: $config->{Version}\n";
 
# Simple values
$config->{Info} = "Write data to JSON";
$config->{Version} = $config->{Version} + 1;
 
# Creating a json object MINISERVER_HASH with nested values 
$config->{MINISERVER_HASH}->{1}->{Name} = "MSOG";
$config->{MINISERVER_HASH}->{2}->{Name} = "MSUG";
 
# Creating an array 'Colors'
my @colors = ( "red", "blue", "green");
$config->{Colors} = \@colors;
 
# Add another color to the array 'Colors'
push @{ $config->{Colors} }, "white";
 
# Creating a json object called 'Server' with some data
my %settings = ( "ip" => "192.168.0.1",
                 "port" => "8000",
                 "protocol" => "tcp"
                );
$config->{Server} = \%settings;
 
# Creating an array of hashes
my @colorspecs;
my %white = (
        name => "White",
        html => "#FFFFFF",
        rgb => "255,255,255",
);
push @colorspecs, \%white;
$config->{ColorSpecs} = \@colorspecs;
 
# Add another hash to the existing array
my %black = (
        name => "Black",
        html => "#000000",
        rgb => "0,0,0",
);
push @{ $config->{ColorSpecs} }, \%black;
 
$jsonparser->write();

JSON Result

{
   "Colors" : [
      "red",
      "blue",
      "green",
      "white"
   ],
   "ColorSpecs" : [
      {
         "html" : "#FFFFFF",
         "name" : "white",
         "rgb" : "255,255,255"
      },
      {
         "html" : "#000000",
         "name" : "black",
         "rgb" : "0,0,0"
      }
   ],
   "Info" : "Write data to JSON",
   "MINISERVERS" : {
      "1" : {
         "Name" : "MSOG"
      },
      "2" : {
         "Name" : "MSUG"
      }
   },
   "Server" : {
      "ip" : "192.168.0.1",
      "port" : "8000",
      "protocol" : "tcp"
   },
   "Version" : 1
}

write()

The write method will check if the content has changed, and only really writes the new content on changes.

write never writes a file:

  • if it has not changed,
  • if readonly ⇒ 1 is set

 From LB 2.2+: write returns 1, if the file was written, and undef if nothing has changed. Before LB2.2,write always returns undef.

File Locking and Exclusive Lock

LoxBerry 2.2.1+

File locking features are enhanced with LoxBerry 2.2.1. The new parameters are ignored below V2.2.1.

LoxBerry::JSON always used shared locking on reading, and exclusive locking on writing, both in blocking mode (wait until lock is successful). This behaviour will stay also with LoxBerry 2.2.1+.

With LoxBerry V2.2.1 these new parameters and features are added:

lockexclusive

$obj->open( filename => "myfile.json", **lockexclusive => 1** );

This will open the file, lock it exclusively, and KEEP IT OPEN LOCKED. Every other process accessing the file will have to wait until the file is unlocked.

Using ->write does not unlock the file.

The file is onlocked if the object is descructed, e.g. with undef $obj, or if it run out of scope.

This only should be used for short read and write operations, but not in a situation where you open the file and keep it open forever.

locktimeout

$obj->open( filename => "myfile.json", **locktimeout => 5** );

Usually, reading and writing use blocking locks, therefore if the file is exclusively locked, read and write need to wait until the file is unlocked (possibly forever).

With the locktimeout in seconds you can define a timeout for waiting for the lock. This is used for both open and write methods. Both functions return undef on error (timeout reached but no lock applied). 

If you use the locktimeout parameter, you should do error handling to realize that your operation failed, especially on write

Closing the file

The file is closed if the $jsonparser object runs out of scope, or you undef the object. Especially with exclusive lock, you should close the file as soon as possible to not interfer with other processes accessing the file.

Example to explicitely close the file

my $jsonparser = LoxBerry::JSON->new();
my $config = $jsonparser->open(filename => "/tmp/somefile.json", writeonclose => 0);

my $variable = $config->{Version};
undef $jsonparser;
# File is closed now

Example of running out of scope

my $version = readversion();
# Function has completed and run out of scope, file is now closed
print $version;

sub readversion 
{
    my $jsonparser = LoxBerry::JSON->new();
    my $config = $jsonparser->open(filename => "/tmp/somefile.json", writeonclose => 0);
    return $config->{Version};
}

Completely replace json content

Whenever you use the jsonparser to open a file, you've get returned a hashref (e.g. $config) to access values by $config→{myvalue}.

In situations where you completely have to remove the content, you cannot set the new content with $config = $newconfig, as this only replaces the link to the data, but the jsonparser still uses the $config reference for saving.

This special access let you change the reference the json parser works with:

$jsonparser->{jsonobj} = $newconfig;

The {jsonobj} is the property the actual reference is stored. Be sure to set this in $jsonparser not $config. The assignment in the example switches the reference to your new config.

Write True and False to json

"true" is not true.

To write true or false, use \1 or \0.

$cfg→{use_http} = \1;

Search in JSON

The module implements a find function to search for elements in hashes and arrays.

my @result = $jsonparser->find(@/%element_to_search, $condition);

Parameter Description
@/%element_to_search The element_to_search must be an ARRAY (@something) or a HASH (%something).

Most likely you would send an object of the readed JSON, e.g. $config→{colors}
$condition This must be a string that contains a valid Perl if condition, that is evaluated. \$_ is the current object in the condition.

Keep an eye on string interpolation:

* Non-escaped sequences are interpolated BEFORE of the function evaluation
* Escaped sequences are interpolated IN the funcation evaluation

Conditions must be able to be evaluated inside an if statement. Precisely it is evaluated as if ($perl_condition).

In the $condition, the $_ is the current evaluated object of the array/hash. Therefore, in your $condition you have to escape the element with \$_ .

In an ARRAY, \$_ therefore is the value of the array.

In an HASH, \$_ is the current evaluated object.

Both datatypes return an array with a list of the keys. In an ARRAY evaluation, the returning array holds the key number (0, 1, 2 etc). In an HASH evaluation, the returning array holds the key name of the found elements.

Double-check escaping in your condition!

my $house = "green";

my $condition_doublequotes = "\$_ eq \"$house\"";  → The function evaluates $_ eq "green" → OK

my $condition_doublequotes = "\$_ eq \$house"; → The function evaluates $_ eq $house. As $house is not defined in the function, it will raise an exception.

my $condition_singlequoutes = '$_ eq "' . $house . '"';  → The function evaluates $_ eq "green" → OK

my $condition_singlequoutes = '$_ eq "$house"';  → The function evaluates $_ eq "$house". As $house is not defined in the function, it will raise an exception.

See the examples.

#!/usr/bin/perl
use LoxBerry::System;
use LoxBerry::JSON;
 
## HASH search ##
 
# Creating a nested hash (1, 2 are names, not array elements)
$config->{MINISERVER_HASH}->{1}->{Name} = "MSOG";
$config->{MINISERVER_HASH}->{2}->{Name} = "MSUG";
 
# Search for a Miniserver named 'MSUG', returns an array with the hash keys
my @result = $jsonparser->find($config->{MINISERVER_HASH}, "\$_->{Name} eq 'MSUG'");
# Dump the result to STDERR
$jsonparser->dump(\@result, "Result of Hash search");
 
## ARRAY search ##
 
# Creating an array 'Colors'
my @colors = ( "red", "blue", "green");
$config->{Colors} = \@colors;
 
# Search for the colors 'red' or 'green', returns an array with the element indexes of the array
my @result = $jsonparser->find($config->{Colors}, "\$_ eq 'red' or \$_ eq 'green'");
# Dump the result to STDERR
$jsonparser->dump(\@result, "Result of Array search");
 
# Delete the element 'blue' in the middle of an array (only the first appearance will be deleted in this example)
my @result = $jsonparser->find($config->{Colors}, "\$_ eq 'blue'");
my $elemKey = $result[0];
splice @{ $config->{Colors} }, $elemKey, 1;
 
# Delete the element 'blue' in the middle of an array (all appearances will be deleted in this example)
my @result = $jsonparser->find($config->{Colors}, "\$_ eq 'blue'");
for my $elemKey (reverse sort @result) { # Reverse order so indices will not change during splicing!
    splice @{ $config->{Colors} }, $elemKey, 1;
}

Parse a JSON

If you would like to parse through a JSON, you should use Perl's key function:

use LoxBerry::JSON;
my $jsonparser = LoxBerry::JSON->new();
my $config = $jsonparser->open(filename => "/tmp/somefile.json");
foreach my $key (keys %$config) {
                print "The key is: " . $key . "\n";
                print "The value is: " . $config->{$key} . "\n";
}

Parse a JSON, sort by an attribute

If you have an unsorted list of objects, you can sort the objects by an attribute of the object:

use LoxBerry::JSON;
my $jsonparser = LoxBerry::JSON->new();
my $config = $jsonparser->open(filename => "/tmp/somefile.json");
foreach my $key ( sort { $config->{$a}->{title} cmp $config->{$b}->{title} } keys %$config) {
                print "The title is: " . $config->{$key}->{title} . "\n";
}

flatten: Get a flat key=value hash from json (available from LoxBerry 2.0)

The flatten function extracts the hierarchie of the json, and returns a flattened hash of the data structure. The delimiter of each hierarchie is the dot (.).

All named elements in the json are named in the hash. All non-named elements (e.g. arrays) will get index numbers as hierarchy. You should use Data::Dumper to view the output.

Example:

use LoxBerry::JSON;
my $jsonparser = LoxBerry::JSON->new();
my $config = $jsonparser->open(filename => "/tmp/somefile.json");
my $hashref = $jsonparser->flatten();
print $hashref->{"SMTP.SMTPSERVER"};
 
# You may want a prefix for your flattened hash
my $hashref = $jsonparser->flatten("CONFIG");
print $hashref->{"CONFIG.SMTP.SMTPSERVER"};

Try the living example in our testing folder: /opt/loxberry/libs/perllib/LoxBerry/testing/json_flat.pl

Use the json content in HTML::Template (available from LoxBerry 2.0)

You can directly assign the json content to HTML::Template by the associate command from HTML::Template:

use HTML::Template;
use LoxBerry::JSON;
my $jsonparser = LoxBerry::JSON->new();
my $config = $jsonparser->open(filename => "/tmp/somefile.json");
 
my $template = HTML::Template->new(
    filename  => 'template.html',
    associate => $jsonparser,
);
 
print $template->output();

Try the living example in our testing folder: /opt/loxberry/libs/perllib/LoxBerry/testing/json_param.pl

encode: Get the current data hash as json (available from LoxBerry 2.0)

To get the current data as json string. The json is not formatted with linebreaks and intends. With the optional parameter pretty ⇒ 1 you get a pretty json with linebreaks and intends.

Keep in mind, that the data is plain json. To use the string in JavaScript, use the method ->jsblock , as it also escapes the json for JavaScript.

use LoxBerry::JSON;
my $jsonparser = LoxBerry::JSON->new();
$jsonparser->open(filename => "/tmp/somefile.json");
 
my $jsonstr = $jsonparser->encode();
my $jsonstr_pretty = $jsonparser->encode( pretty => 1 );

Try the living example in our testing folder: /opt/loxberry/libs/perllib/LoxBerry/testing/json_encode.pl

jsblock: Generate a ready string to insert into your <script> block (available from LoxBerry 2.0)

The method returns a finally and ready-escaped varname = JSON.parse(...); statement to print into your JavaScript block. Use the parameter varname => "jsvariablename" to set the JS variable name.

use LoxBerry::JSON;
my $jsonparser = LoxBerry::JSON->new();
$jsonparser->open(filename => "/tmp/somefile.json");
 
print "<script>";
print $jsonparser->jsblock( varname => "config" );
print "console.log(config.SMTP.EMAIL);"
print "</script>";

Result of this code:

<script>
config = JSON.parse('{"SMTP":{"EMAIL":"my@mail.com"}}');
console.log(config.SMTP.EMAIL);
</script>

Try the living example in our testing folder: /opt/loxberry/libs/perllib/LoxBerry/testing/json_jsblock.cgi