| ConfigEntry.java |
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 as published by
5 * the Free Software Foundation. This program is distributed in the hope
6 * that it will be useful, but WITHOUT ANY WARRANTY; without even the
7 * 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
18 * The copyright to this program is held by it's authors.
19 *
20 * ID: $Id: ConfigEntry.java 2146 2011-04-07 20:04:54Z dmsmith $
21 */
22 package org.crosswire.jsword.book.sword;
23
24 import java.util.ArrayList;
25 import java.util.List;
26 import java.util.regex.Pattern;
27
28 import org.crosswire.common.util.Histogram;
29 import org.crosswire.common.util.Logger;
30 import org.crosswire.common.util.StringUtil;
31 import org.crosswire.common.xml.XMLUtil;
32 import org.crosswire.jsword.book.OSISUtil;
33 import org.jdom.Element;
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(report("Ignoring unexpected RTF for", getName(), 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(report("Ignoring unknown config value for", confEntryName, aValue));
226 return;
227 }
228 values.add(aValue);
229 } else {
230 if (value != null) {
231 log.info(report("Ignoring unexpected additional entry for", confEntryName, aValue));
232 } else {
233 histogram.increment(confEntryName);
234 if (type.hasChoices()) {
235 histogram.increment(confEntryName + '.' + aValue);
236 }
237 if (!isAllowed(aValue)) {
238 log.info(report("Ignoring unknown config value for", confEntryName, 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 private String report(String issue, String confEntryName, String aValue) {
407 StringBuilder buf = new StringBuilder(100);
408 buf.append(issue);
409 buf.append(' ');
410 buf.append(confEntryName);
411 buf.append(" in ");
412 buf.append(internal);
413 buf.append(": ");
414 buf.append(aValue);
415
416 return buf.toString();
417 }
418
419 /**
420 * The log stream
421 */
422 private static final Logger log = Logger.getLogger(ConfigEntry.class);
423
424 /**
425 * A pattern of allowable RTF in a SWORD conf. These are: \pard, \pae, \par,
426 * \qc \b, \i and embedded Unicode
427 */
428 private static final Pattern RTF_PATTERN = Pattern.compile("\\\\pard|\\\\pa[er]|\\\\qc|\\\\[bi]|\\\\u-?[0-9]{4,6}+");
429
430 /**
431 * A histogram for debugging.
432 */
433 private static Histogram histogram = new Histogram();
434
435 private ConfigEntryType type;
436 private String internal;
437 private String name;
438 private List<String> values;
439 private Object value;
440 }
441