1 /*
2   Copyright (c) 2006 - 2021
3   CLST  - Radboud University
4   ILK   - Tilburg University
5 
6   This file is part of ticcutils
7 
8   ticcutils is free software; you can redistribute it and/or modify
9   it under the terms of the GNU General Public License as published by
10   the Free Software Foundation; either version 3 of the License, or
11   (at your option) any later version.
12 
13   ticcutils is distributed in the hope that it will be useful,
14   but WITHOUT ANY WARRANTY; without even the implied warranty of
15   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16   GNU General Public License for more details.
17 
18   You should have received a copy of the GNU General Public License
19   along with this program; if not, see <http://www.gnu.org/licenses/>.
20 
21   For questions and suggestions, see:
22       https://github.com/LanguageMachines/ticcutils/issues
23   or send mail to:
24       lamasoftware (at ) science.ru.nl
25 
26 */
27 
28 #include "ticcutils/Configuration.h"
29 
30 #include <string>
31 #include <map>
32 #include <set>
33 #include <fstream>
34 #include <stdexcept>
35 #include <iostream>
36 #include "ticcutils/StringOps.h"
37 
38 using namespace std;
39 
40 namespace TiCC {
41 
Configuration()42   Configuration::Configuration(){
43     /// set up a Configuration structure
44     myMap["global"] = ssMap();
45   }
46 
fixControl(const string & s,char c)47   string fixControl( const string& s, char c ){
48     /// replace special characters
49     /*!
50       \param s the inputstring
51       \param c special 'symbol' to replace
52       \return a new string where all occurences of "\\c" aee replaced  by "\c"
53     */
54     string sString;
55     string rString;
56     switch ( c ){
57     case 't':
58       sString = "\\t";
59       rString = "\t";
60       break;
61     case 'r':
62       sString = "\\r";
63       rString = "\r";
64       break;
65     case 'n':
66       sString = "\\n";
67       rString = "\n";
68       break;
69     default:
70       throw logic_error("invalid char for fixControl" );
71     }
72     string::size_type pos1 = s.find( sString );
73     if ( pos1 == string::npos ){
74       return s;
75     }
76     else {
77       string result = s.substr( 0, pos1 );
78       result += rString;
79       string::size_type pos2 = s.find( sString, pos1+1 );
80       while ( pos2 != string::npos ){
81 	result += s.substr( pos1+2, pos2-pos1-2 );
82 	result += rString;
83 	pos1 = pos2;
84 	pos2 = s.find( sString, pos1+1 );
85       }
86       result += s.substr( pos1+2 );
87       return result;
88     }
89   }
90 
fixControls(const string & s)91   string fixControls( const string& s ){
92     /// replace all "\\n", "\\r", "\\t" sequences by "\n", "\r" and "\t"
93     string result = s;
94     result = fixControl( result, 'n' );
95     result = fixControl( result, 'r' );
96     result = fixControl( result, 't' );
97     return result;
98   }
99 
get_att_val(const string & line,const string & section)100   bool Configuration::get_att_val( const string& line, const string& section ){
101     /// parse a string into an attribute/value pair and insert in a section
102     /*!
103       \param line the line to parse
104       \param section the section to insert in.
105       \return true if we have a result
106      */
107     string::size_type pos = line.find("=");
108     if ( pos != string::npos ){
109       string att = line.substr(0,pos);
110       att = TiCC::trim(att);
111       string val = line.substr(pos+1);
112       val = TiCC::trim(val);
113       if ( val[0] == '"' && val[val.length()-1] == '"' ){
114 	val = val.substr(1, val.length()-2);
115       }
116       val = fixControls( val );
117       myMap[section][att] = val;
118       return true;
119     }
120     return false;
121   }
122 
fill(const string & fileName)123   bool Configuration::fill( const string& fileName ){
124     /// fill a Configuration structure from a file
125     /*!
126       \param fileName the name of the input file
127       \return true on succes
128     */
129     ifstream is( fileName );
130     if ( !is ){
131       cerr << "unable to read configuration from " << fileName << endl;
132       return false;
133     }
134     string cdir = dirname( fileName );
135     if ( cdir == "." ){
136       cdir = "";
137     }
138     else {
139       cdir += "/";
140     }
141     //  cerr << "dirname= " << cdir << endl;
142     myMap["global"]["configDir"] = cdir; // can be overidden below
143     string inLine;
144     string section = "global";
145     while ( getline( is, inLine ) ){
146       string line = TiCC::trim(inLine);
147       if ( line.empty() || line[0] == '#' ){
148 	continue;
149       }
150       if ( match_front( line, "[[" ) ){
151 	if ( line[line.length()-1] == ']' &&
152 	     line[line.length()-2] == ']' ){
153 	  section = line.substr(2,line.length()-4);
154 	  //	cerr << "GOT section = " << section << endl;
155 	}
156 	else {
157 	  cerr << "invalid section: in line '" << line << "'" << endl;
158 	  return false;
159 	}
160       }
161       else if ( !get_att_val ( line, section ) ){
162 	cerr << "invalid attribute value pair in line '"
163 	     << line << "'" << endl;
164 	return false;
165       }
166     }
167     return true;
168   }
169 
fill(const string & fileName,const string & insect)170   bool Configuration::fill( const string& fileName, const string& insect ){
171     /// fill a Configuration structure from a file for a certain section
172     /*!
173       \param fileName the name of the input file
174       \param insect the section to find and fill
175       \return true on succes
176     */
177     ifstream is( fileName );
178     if ( !is ){
179       cerr << "unable to read configuration from " << fileName << endl;
180       return false;
181     }
182     bool found = false;
183     string inLine;
184     string localsection;
185     string section = TiCC::trim(insect);
186     //  cerr << "looking for section = " << insection << endl;
187     while ( getline( is, inLine ) ){
188       string line = TiCC::trim(inLine);
189       if ( line.empty() || line[0] == '#' ){
190 	continue;
191       }
192       if ( match_front( line, "[[" ) ){
193 	if ( line[line.length()-1] == ']' &&
194 	     line[line.length()-2] == ']' ){
195 	  localsection = line.substr(2,line.length()-4);
196 	  localsection = TiCC::trim(localsection);
197 	  //	cerr << "GOT section = " << localsection << endl;
198 	}
199 	else {
200 	  cerr << "invalid section: in line '" << line << "'" << endl;
201 	  return false;
202 	}
203       }
204       else if ( localsection == section ){
205 	found = true;
206 	if ( !get_att_val( line, section ) ){
207 	  cerr << "invalid attribute value pair in line '"
208 	       << line << "'" << endl;
209 	  return false;
210 	}
211       }
212     }
213     if ( !found ){
214       cerr << "unable to find a section [[" << section << "]] in file: "
215 	   << fileName << endl;
216       return false;
217     }
218     return true;
219   }
220 
encode_ctrl(const string & in)221   string encode_ctrl( const string& in ){
222     /// encdode "\n", "\r" and "\t" into "\\n", "\\r" and "\\t"
223     /*!
224       \param in the input string
225       \return an encodes string
226     */
227     string out;
228     for ( const auto& c : in ){
229       switch ( c ){
230       case '\n': out += "\\n";
231 	break;
232       case '\r': out += "\\r";
233 	break;
234       case '\t': out += "\\t";
235 	break;
236       default:
237 	out += c;
238       }
239     }
240     return out;
241   }
242 
dump(ostream & os) const243   void Configuration::dump( ostream& os ) const {
244     /// dump a Configuration to a stream
245     auto it1 = myMap.find("global");
246     if ( it1 == myMap.end() ){
247       os << "empty" << endl;
248       return;
249     }
250     os << "[[global]]" << endl;
251     auto it2 = it1->second.begin();
252     while ( it2 != it1->second.end() ){
253       os << it2->first << "=" << it2->second << endl;
254       ++it2;
255     }
256     it1 = myMap.begin();
257     while ( it1 != myMap.end() ){
258       if ( it1->first != "global" ){
259 	os << endl << "[[" << it1->first << "]]" << endl;
260 	it2 = it1->second.begin();
261 	while ( it2 != it1->second.end() ){
262 	  os << it2->first << "=" << encode_ctrl(it2->second) << endl;
263 	  ++it2;
264 	}
265       }
266       ++it1;
267     }
268   }
269 
create_configfile(const string & name) const270   void Configuration::create_configfile( const string& name ) const {
271     /// create a named configfile
272     /*!
273       \param name the name of the output file
274     */
275     ofstream os( name );
276     if ( !os ){
277       throw runtime_error( "unable to create outputfile: " + name );
278     }
279     dump( os );
280   }
281 
setatt(const string & inatt,const string & inval,const string & insect)282   string Configuration::setatt( const string& inatt,
283 				const string& inval,
284 				const string& insect ){
285     /// set an attribute/value pair in a section
286     /*!
287       \param inatt the attribute to add
288       \param inval the value to insert
289       \param insect the section to insert in. When empty, use the "global" one.
290       \return the old value if \e inatt was already set.
291      */
292     string oldVal;
293     string att = TiCC::trim(inatt);
294     string val = TiCC::trim(inval);
295     string sect = TiCC::trim(insect);
296     if ( sect.empty() ){
297       sect = "global";
298     }
299     auto it1 = myMap.find( sect );
300     if ( it1 != myMap.end() ){
301       auto const& it2 = it1->second.find( att );
302       if ( it2 != it1->second.end() ){
303 	oldVal = it2->second;
304       }
305       it1->second[att] = val;
306     }
307     else {
308       myMap[sect].insert( make_pair( att, val ) );
309     }
310     return oldVal;
311   }
312 
clearatt(const string & inatt,const string & insect)313   string Configuration::clearatt( const string& inatt,
314 				  const string& insect ){
315     /// remove an attribute (and its value) from a section
316     /*!
317       \param inatt the attribute to remove
318       \param insect the section to search,  When empty, use the "global" one.
319       \return the value of the removed attribute
320     */
321     //    cerr << "clear att: '" << inatt << "' in [" << insect << "]" << endl;
322     string oldVal;
323     string sect = TiCC::trim(insect);
324     string att = TiCC::trim(inatt);
325     if ( sect.empty() ){
326       sect = "global";
327     }
328     //    cerr << "clear sect [" << sect << "]" << endl;
329     auto it1 = myMap.find( sect );
330     if ( it1 != myMap.end() ){
331       //      cerr << "found sect [" << sect << "]" << endl;
332       auto const& it2 = it1->second.find( att );
333       if ( it2 != it1->second.end() ){
334 	//	cerr << "found att [" << att << "]" << endl;
335 	oldVal = it2->second;
336       }
337       else {
338 	//	cerr << "missed att [" << att << "]" << endl;
339       }
340       it1->second.erase( att );
341     }
342     else {
343       //      cerr << "MISSED sect [" << sect << "]" << endl;
344     }
345     return oldVal;
346   }
347 
erasesection(const string & insect)348   void Configuration::erasesection( const string& insect ){
349     /// remove all attributes from a section and removes the section
350     /*!
351       \param insect the section to remove.
352     */
353     myMap.erase( insect );
354   }
355 
getatt(const string & inatt,const string & insect) const356   string Configuration::getatt( const string& inatt,
357 				const string& insect ) const {
358     /// return the value for an attribute
359     /*!
360       \param inatt the attribute to remove
361       \param insect the section to search,  When empty, use the "global" one.
362       \return the value of the attribute
363       \note when the attribute is NOT FOUND in the given section, we try to
364       retrieve it from the 'global' one. Which serves as a fallback then
365     */
366     string sect = TiCC::trim(insect);
367     string att = TiCC::trim(inatt);
368     string key = sect;
369     if ( key.empty() ){
370       key = "global";
371     }
372     auto const& it1 = myMap.find( key );
373     if ( it1 == myMap.end() ){
374       return "";
375     }
376     else {
377       auto const& it2 = it1->second.find( att );
378       if ( it2 == it1->second.end() ){
379 	if ( sect.empty() || sect == "global" ){
380 	  return "";
381 	}
382 	else {
383 	  return getatt( att, "global" );
384 	}
385       }
386       else {
387 	return it2->second;
388       }
389     }
390   }
391 
lookUpAll(const string & insect) const392   map<string,string> Configuration::lookUpAll( const string& insect ) const {
393     /// return all attribute/value values in a given section
394     /*!
395       \param insect the section to search. When empty use 'global'
396       \return a map of all attribute/value pairs
397     */
398     map<string,string> result;
399     string sect = TiCC::trim(insect);
400     if ( sect.empty() ){
401       sect = "global";
402     }
403     auto const& it1 = myMap.find( sect );
404     if ( it1 != myMap.end() ){
405       auto it2 = it1->second.begin();
406       while ( it2 != it1->second.end() ){
407 	result[it2->first] = it2->second;
408 	++it2;
409       }
410     }
411     return result;
412   }
413 
lookUpSections() const414   set<string> Configuration::lookUpSections() const {
415     /// return the names of all sections
416     set<string> result;
417     result.insert("global");
418     auto it = myMap.begin();
419     while ( it != myMap.end() ){
420       result.insert( it->first );
421       ++it;
422     }
423     return result;
424   }
425 
hasSection(const string & insect) const426   bool Configuration::hasSection( const string& insect ) const {
427     /// check the presence of a section
428     string sect = TiCC::trim( insect );
429     if ( !sect.empty() ){
430       auto const it = myMap.find( sect );
431       if ( it != myMap.end() ){
432 	return true;
433       }
434     }
435     return false;
436   }
437 
merge(const Configuration & in,bool override)438   void Configuration::merge( const Configuration& in, bool override ) {
439     /// merge two Configuration objects
440     /*!
441       \param in the Configuration to add
442       \param override when true, override allready present attributes
443      */
444     // get al sections from in;
445     set<string> sections = in.lookUpSections();
446     for ( const auto& s : sections ){
447       // for every section, get all at-val pairs
448       auto avs = in.lookUpAll( s );
449       for ( const auto& av : avs ){
450 	// merge every at-val in the wanted section
451 	if ( !override
452 	     &&  myMap[s].find( av.first ) != myMap[s].end() ){
453 	  continue;
454 	}
455 	setatt( av.first, av.second, s );
456       }
457     }
458   }
459 
460 }
461