1   /**
2    * Distribution License:
3    * JSword is free software; you can redistribute it and/or modify it under
4    * the terms of the GNU Lesser General Public License, version 2.1 or later
5    * as published by the Free Software Foundation. This program is distributed
6    * in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
7    * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
8    * See the GNU Lesser General Public License for more details.
9    *
10   * The License is available on the internet at:
11   *       http://www.gnu.org/copyleft/lgpl.html
12   * or by writing to:
13   *      Free Software Foundation, Inc.
14   *      59 Temple Place - Suite 330
15   *      Boston, MA 02111-1307, USA
16   *
17   * Copyright: 2005-2013
18   *     The copyright to this program is held by it's authors.
19   *
20   */
21  package org.crosswire.jsword.book.sword;
22  
23  import java.util.ArrayList;
24  import java.util.List;
25  import java.util.regex.Pattern;
26  
27  import org.crosswire.common.util.Histogram;
28  import org.crosswire.common.util.StringUtil;
29  import org.crosswire.common.xml.XMLUtil;
30  import org.crosswire.jsword.book.OSISUtil;
31  import org.jdom2.Element;
32  import org.slf4j.Logger;
33  import org.slf4j.LoggerFactory;
34  
35  /**
36   * A ConfigEntry holds the value(s) for an entry of ConfigEntryType.
37   * 
38   * @see gnu.lgpl.License for license details.<br>
39   *      The copyright to this program is held by it's authors.
40   * @see gnu.lgpl.License
41   * @author DM Smith [ dmsmith555 at yahoo dot com]
42   */
43  public final class ConfigEntry {
44  
45      /**
46       * Create a ConfigEntry whose type is not certain and whose value is not
47       * known.
48       * 
49       * @param bookName
50       *            the internal name of the book
51       * @param aName
52       *            the name of the ConfigEntry.
53       */
54      public ConfigEntry(String bookName, String aName) {
55          internal = bookName;
56          name = aName;
57          type = ConfigEntryType.fromString(aName);
58      }
59  
60      /**
61       * Create a ConfigEntry directly with an initial value.
62       * 
63       * @param bookName
64       *            the internal name of the book
65       * @param aType
66       *            the kind of ConfigEntry
67       * @param aValue
68       *            the initial value for the ConfigEntry
69       */
70      public ConfigEntry(String bookName, ConfigEntryType aType, String aValue) {
71          internal = bookName;
72          name = aType.getName();
73          type = aType;
74          addValue(aValue);
75      }
76  
77      /**
78       * Get the key of this ConfigEntry
79       */
80      public String getName() {
81          if (type != null) {
82              return type.getName();
83          }
84          return name;
85      }
86  
87      /**
88       * Get the type of this ConfigEntry
89       */
90      public ConfigEntryType getType() {
91          return type;
92      }
93  
94      /**
95       * Determines whether the string is allowed. For some config entries, the
96       * value is expected to be one of a group, for others the format is defined.
97       * 
98       * @param aValue
99       * @return true if the string is allowed
100      */
101     public boolean isAllowed(String aValue) {
102         if (type != null) {
103             return type.isAllowed(aValue);
104         }
105         return true;
106     }
107 
108     /**
109      * RTF is allowed in a few config entries.
110      * 
111      * @return true if RTF is allowed
112      */
113     public boolean allowsRTF() {
114         if (type != null) {
115             return type.allowsRTF();
116         }
117         return true;
118     }
119 
120     /**
121      * While most fields are single line or single value, some allow
122      * continuation. A continuation mark is a backslash at the end of a line. It
123      * is not to be followed by whitespace.
124      * 
125      * @return true if continuation is allowed
126      */
127     public boolean allowsContinuation() {
128         if (type != null) {
129             return type.allowsContinuation();
130         }
131         return true;
132     }
133 
134     /**
135      * Some keys can repeat. When this happens each is a single value pick from
136      * a list of choices.
137      * 
138      * @return true if this ConfigEntryType can occur more than once
139      */
140     public boolean mayRepeat() {
141         if (type != null) {
142             return type.mayRepeat();
143         }
144         return true;
145     }
146 
147     /**
148      *
149      */
150     public boolean reportDetails() {
151         if (type != null) {
152             return type.reportDetails();
153         }
154         return true;
155     }
156 
157     /**
158      * Determine whether this config entry is supported.
159      * 
160      * @return true if this ConfigEntry has a type.
161      */
162     public boolean isSupported() {
163         return type != null;
164     }
165 
166     /**
167      * Get the value(s) of this ConfigEntry. If mayRepeat() == true then it
168      * returns a List. Otherwise it returns a string.
169      * 
170      * @return a list, value or null.
171      */
172     public Object getValue() {
173         if (value != null) {
174             return value;
175         }
176         if (values != null) {
177             return values;
178         }
179         return type.getDefault();
180     }
181 
182     /**
183      * Determine whether this Config entry matches the value.
184      * 
185      * @param search
186      *            the value to match against
187      * @return true if this ConfigEntry matches the value
188      */
189     public boolean match(Object search) {
190         if (value != null) {
191             return value.equals(search);
192         }
193         if (values != null) {
194             return values.contains(search);
195         }
196         Object def = type.getDefault();
197         return def != null && def.equals(search);
198     }
199 
200     /**
201      * Add a value to the list of values for this ConfigEntry
202      */
203     public void addValue(String val) {
204         String aValue = val;
205         String confEntryName = getName();
206         // Filter known types of entries
207         if (type != null) {
208             aValue = type.filter(aValue);
209         }
210 
211         // Report on fields that shouldn't have RTF but do
212         if (!allowsRTF() && RTF_PATTERN.matcher(aValue).find()) {
213             log.info("Ignoring unexpected RTF for {} in {}: {}", confEntryName, internal, aValue);
214         }
215 
216         if (mayRepeat()) {
217             if (values == null) {
218                 histogram.increment(confEntryName);
219                 values = new ArrayList<String>();
220             }
221             if (reportDetails()) {
222                 histogram.increment(confEntryName + '.' + aValue);
223             }
224             if (!isAllowed(aValue)) {
225                 log.info("Ignoring unknown config value for {} in {}: {}", confEntryName, internal, aValue);
226                 return;
227             }
228             values.add(aValue);
229         } else {
230             if (value != null) {
231                 log.info("Ignoring unexpected additional entry for {} in {}: {}", confEntryName, internal, aValue);
232             } else {
233                 histogram.increment(confEntryName);
234                 if (type.hasChoices()) {
235                     histogram.increment(confEntryName + '.' + aValue);
236                 }
237                 if (!isAllowed(aValue)) {
238                     log.info("Ignoring unknown config value for {} in {}: {}", confEntryName, internal, aValue);
239                     return;
240                 }
241                 value = type.convert(aValue);
242             }
243         }
244     }
245 
246     public Element toOSIS() {
247         OSISUtil.OSISFactory factory = OSISUtil.factory();
248 
249         Element rowEle = factory.createRow();
250 
251         Element nameEle = factory.createCell();
252         Element hiEle = factory.createHI();
253         hiEle.setAttribute(OSISUtil.OSIS_ATTR_TYPE, OSISUtil.HI_BOLD);
254         nameEle.addContent(hiEle);
255         Element valueElement = factory.createCell();
256         rowEle.addContent(nameEle);
257         rowEle.addContent(valueElement);
258 
259         // I18N(DMS): use name to lookup translation.
260         hiEle.addContent(getName());
261 
262         if (value != null) {
263             String text = value.toString();
264             text = XMLUtil.escape(text);
265             if (allowsRTF()) {
266                 valueElement.addContent(OSISUtil.rtfToOsis(text));
267             } else if (allowsContinuation()) {
268                 valueElement.addContent(processLines(factory, text));
269             } else {
270                 valueElement.addContent(text);
271             }
272         }
273 
274         if (values != null) {
275             Element listEle = factory.createLG();
276             valueElement.addContent(listEle);
277 
278             for (String str : values) {
279                 String text = XMLUtil.escape(str);
280                 Element itemEle = factory.createL();
281                 listEle.addContent(itemEle);
282                 if (allowsRTF()) {
283                     itemEle.addContent(OSISUtil.rtfToOsis(text));
284                 } else {
285                     itemEle.addContent(text);
286                 }
287             }
288         }
289         return rowEle;
290     }
291 
292     public static void resetStatistics() {
293         histogram.clear();
294     }
295 
296     public static void dumpStatistics() {
297         // Uncomment the following line to produce statistics
298         // System.out.println(histogram.toString());
299     }
300 
301     @Override
302     public boolean equals(Object obj) {
303         // Since this can not be null
304         if (obj == null) {
305             return false;
306         }
307 
308         // Check that that is the same as this
309         // Don't use instanceOf since that breaks inheritance
310         if (!obj.getClass().equals(this.getClass())) {
311             return false;
312         }
313 
314         ConfigEntry that = (ConfigEntry) obj;
315         return that.getName().equals(this.getName());
316     }
317 
318     @Override
319     public int hashCode() {
320         return getName().hashCode();
321     }
322 
323     @Override
324     public String toString() {
325         return getName();
326     }
327 
328     /**
329      * Build's a SWORD conf file as a string. The result is not identical to the
330      * original, cleaning up problems in the original and re-arranging the
331      * entries into a predictable order.
332      * 
333      * @return the well-formed conf.
334      */
335     public String toConf() {
336         StringBuilder buf = new StringBuilder();
337 
338         if (value != null) {
339             buf.append(getName());
340             buf.append('=');
341             String text = getConfValue(value);
342             if (allowsContinuation()) {
343                 // With continuation each line is ended with a '\', except the
344                 // last.
345                 text = text.replaceAll("\n", "\\\\\n");
346             }
347             buf.append(text);
348             buf.append('\n');
349         } else if (type.equals(ConfigEntryType.CIPHER_KEY)) {
350             // CipherKey is empty to indicate that it is encrypted and locked.
351             buf.append(getName());
352             buf.append('=');
353         }
354 
355         if (values != null) {
356             // History values begin with the history value, e.g. 1.2
357             // followed by a space.
358             // These are to joined to the key.
359             if (type.equals(ConfigEntryType.HISTORY)) {
360                 for (String text : values) {
361                     buf.append(getName());
362                     buf.append('_');
363                     buf.append(text.replaceFirst(" ", "="));
364                     buf.append('\n');
365                 }
366             } else {
367                 for (String text : values) {
368                     buf.append(getName());
369                     buf.append('=');
370                     buf.append(getConfValue(text));
371                     buf.append('\n');
372                 }
373             }
374         }
375         return buf.toString();
376     }
377 
378     /**
379      * The conf value is the internal representation of the string.
380      * 
381      * @param aValue
382      *            either value or values[i]
383      * @return the conf value.
384      */
385     private String getConfValue(Object aValue) {
386         if (aValue != null) {
387             if (type != null) {
388                 return type.unconvert(aValue);
389             }
390             return aValue.toString();
391         }
392         return null;
393     }
394 
395     private List<Element> processLines(OSISUtil.OSISFactory factory, String aValue) {
396         List<Element> list = new ArrayList<Element>();
397         String[] lines = StringUtil.splitAll(aValue, '\n');
398         for (int i = 0; i < lines.length; i++) {
399             Element lineElement = factory.createL();
400             lineElement.addContent(lines[i]);
401             list.add(lineElement);
402         }
403         return list;
404     }
405 
406     /**
407      * A pattern of allowable RTF in a SWORD conf. These are: \pard, \pae, \par,
408      * \qc \b, \i and embedded Unicode
409      */
410     private static final Pattern RTF_PATTERN = Pattern.compile("\\\\pard|\\\\pa[er]|\\\\qc|\\\\[bi]|\\\\u-?[0-9]{4,6}+");
411 
412     /**
413      * A histogram for debugging.
414      */
415     private static Histogram histogram = new Histogram();
416 
417     private ConfigEntryType type;
418     private String internal;
419     private String name;
420     private List<String> values;
421     private Object value;
422 
423     /**
424      * The log stream
425      */
426     private static final Logger log = LoggerFactory.getLogger(ConfigEntry.class);
427 }
428