001: package org.tigris.scarab.util.build.l10nchecker;
002:
003: /* ================================================================
004: * Copyright (c) 2005 CollabNet. All rights reserved.
005: *
006: * Redistribution and use in source and binary forms, with or without
007: * modification, are permitted provided that the following conditions are
008: * met:
009: *
010: * 1. Redistributions of source code must retain the above copyright
011: * notice, this list of conditions and the following disclaimer.
012: *
013: * 2. Redistributions in binary form must reproduce the above copyright
014: * notice, this list of conditions and the following disclaimer in the
015: * documentation and/or other materials provided with the distribution.
016: *
017: * 3. The end-user documentation included with the redistribution, if
018: * any, must include the following acknowlegement: "This product includes
019: * software developed by Collab.Net <http://www.Collab.Net/>."
020: * Alternately, this acknowlegement may appear in the software itself, if
021: * and wherever such third-party acknowlegements normally appear.
022: *
023: * 4. The hosted project names must not be used to endorse or promote
024: * products derived from this software without prior written
025: * permission. For written permission, please contact info@collab.net.
026: *
027: * 5. Products derived from this software may not use the "Tigris" or
028: * "Scarab" names nor may "Tigris" or "Scarab" appear in their names without
029: * prior written permission of Collab.Net.
030: *
031: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
032: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
033: * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
034: * IN NO EVENT SHALL COLLAB.NET OR ITS CONTRIBUTORS BE LIABLE FOR ANY
035: * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
036: * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
037: * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
038: * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
039: * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
040: * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
041: * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
042: *
043: * ====================================================================
044: *
045: * This software consists of voluntary contributions made by many
046: * individuals on behalf of Collab.Net.
047: */
048:
049: import java.io.BufferedReader;
050: import java.io.File;
051: import java.io.FileReader;
052: import java.io.IOException;
053: import java.text.MessageFormat;
054: import java.util.ArrayList;
055: import java.util.Hashtable;
056: import java.util.Iterator;
057: import java.util.List;
058:
059: import org.apache.oro.text.regex.MalformedPatternException;
060: import org.apache.oro.text.regex.MatchResult;
061: import org.apache.oro.text.regex.Pattern;
062: import org.apache.oro.text.regex.Perl5Compiler;
063: import org.apache.oro.text.regex.Perl5Matcher;
064:
065: import org.tigris.scarab.util.Log;
066: import org.tigris.scarab.util.build.l10nchecker.issues.CantParseLineIssue;
067: import org.tigris.scarab.util.build.l10nchecker.issues.DefinedTwiceIssue;
068: import org.tigris.scarab.util.build.l10nchecker.issues.DifferentAttributeCountIssue;
069: import org.tigris.scarab.util.build.l10nchecker.issues.IllegalPatternIssue;
070: import org.tigris.scarab.util.build.l10nchecker.issues.NoTransAllowedIssue;
071: import org.tigris.scarab.util.build.l10nchecker.issues.NotInReferenceIssue;
072: import org.tigris.scarab.util.build.l10nchecker.issues.NotTranslatedIssue;
073: import org.tigris.scarab.util.build.l10nchecker.issues.TranslatedTwiceDiffIssue;
074: import org.tigris.scarab.util.build.l10nchecker.issues.TranslatedTwiceIssue;
075: import org.tigris.scarab.util.build.l10nchecker.issues.TranslationMissingIssue;
076: import org.tigris.scarab.util.build.l10nchecker.issues.TranslationRequiredIssue;
077:
078: /**
079: * Scarab language checker.
080: *
081: * <p>
082: * A L10nInspector represents a class that is reading a property file (see
083: * {@link #setReference(String)}) containing l10n properties as a reference and then
084: * running through a given list of other property files to check if all
085: * localisations that are needed are given.
086: *
087: * <p>
088: * A property language files that need to be checked against can be passed the
089: * reference file can be set by {@link #checkFile(String)}.
090: *
091: * <p>
092: * The results can be retrieved by using the get* methods.
093: *
094: * @author sreindl
095: */
096: public class L10nInspector {
097:
098: /* regular expression matching stuff */
099: private static String COMMENT_TRANS = "^\\s*#(\\+|-)TRANS.*$";
100:
101: private static String COMMENT_REGEX = "^\\s*(#.*)?$";
102:
103: private static String COMMAND_REGEX = "^\\s*([^=\\s]+)\\s*=\\s*(.*)$";
104:
105: private static Perl5Compiler compiler = new Perl5Compiler();
106:
107: private static Perl5Matcher matcher = new Perl5Matcher();
108:
109: private static Pattern commentPattern = null;
110:
111: private static Pattern commandPattern = null;
112:
113: private static Pattern transPattern = null;
114:
115: /* properties of reference file */
116: private Hashtable refProperties;
117:
118: /* filename of reference file */
119: private String refFileName;
120:
121: /* filename of file to be checked */
122: private String checkFileName;
123:
124: /* statistical information */
125: private int linesRead = 0;
126:
127: /* messages generated during parsing */
128: private List messages = null;
129:
130: /**
131: * Create a standard instance
132: */
133: public L10nInspector() throws MalformedPatternException {
134: try {
135: L10nIssueTemplates.reset();
136: commentPattern = compiler.compile(COMMENT_REGEX);
137: commandPattern = compiler.compile(COMMAND_REGEX);
138: transPattern = compiler.compile(COMMENT_TRANS);
139: } catch (MalformedPatternException exMP) {
140: Log.get().fatal(exMP);
141: throw exMP; // rethrow
142: }
143: messages = new ArrayList();
144: }
145:
146: /**
147: * Set the reference file to be used. This call resets all internal
148: * variables and resets all counters
149: *
150: * @param aRefFile
151: * The file to be used as a reference
152: *
153: * @throws IOException
154: * In case the file cannot be read.
155: */
156: public int setReference(String aRefFile) throws IOException {
157: refFileName = aRefFile;
158:
159: refProperties = new Hashtable();
160: File inFile = new File(refFileName);
161: if (!inFile.canRead()) {
162: throw new IOException("Cannot read reference file "
163: + aRefFile);
164: }
165: loadReferenceFile(inFile);
166: return refProperties.size();
167: }
168:
169: /**
170: * Function to return only the errors that occured during parsing
171: *
172: * @return Returns the errors.
173: */
174: public List getErrors() {
175: List errs = new ArrayList(messages.size());
176: Iterator it = messages.iterator();
177:
178: while (it.hasNext()) {
179: L10nMessage msg = (L10nMessage) it.next();
180: if (msg.getIssue().isError()) {
181: errs.add(msg);
182: }
183: }
184: return errs;
185: }
186:
187: /**
188: * Return a list of warnings generated during processing
189: *
190: * @return Returns the warnings.
191: */
192: public List getWarnings() {
193: List warnings = new ArrayList(messages.size());
194: Iterator it = messages.iterator();
195:
196: while (it.hasNext()) {
197: L10nMessage msg = (L10nMessage) it.next();
198: if (msg.getIssue().isWarning()) {
199: warnings.add(msg);
200: }
201: }
202: return warnings;
203: }
204:
205: /**
206: * Generate a list of information messages generated during processing.
207: *
208: * @return Returns the information messages.
209: */
210: public List getInfos() {
211: List infos = new ArrayList(messages.size());
212: Iterator it = messages.iterator();
213:
214: while (it.hasNext()) {
215: L10nMessage msg = (L10nMessage) it.next();
216: if (msg.getIssue().isInfo()) {
217: infos.add(msg);
218: }
219: }
220: return infos;
221: }
222:
223: /**
224: * Return the messages collected.
225: *
226: * @return messages
227: */
228: public List getMessages() {
229: return messages;
230: }
231:
232: /**
233: * Check if the parsing/checking resulted in errors.
234: *
235: * @return true if errors have been seen during loading/parsing
236: */
237: public boolean hasErrors() {
238: return getErrors().size() > 0;
239: }
240:
241: /**
242: * Check an language file.
243: *
244: * <p>
245: * TODO: Sophisticated exception handling
246: *
247: * @return Number of (unique)translation entries
248: */
249: public int checkFile(String filename) throws IOException {
250: BufferedReader inStream = null;
251: String inLine;
252: Hashtable seen = new Hashtable();
253: int lineNo = 0;
254: boolean doNotTrans = false;
255:
256: messages.clear();
257: checkFileName = filename;
258: try {
259: inStream = new BufferedReader(new FileReader(filename));
260: while ((inLine = inStream.readLine()) != null) {
261: lineNo++;
262: if (matcher.matches(inLine, transPattern)) {
263: MatchResult result = matcher.getMatch();
264: if (result.group(1).equals("-")) {
265: doNotTrans = true;
266: }
267: } else if (matcher.matches(inLine, commentPattern)) {
268: // skip comment lines
269: continue;
270: } else if (matcher.contains(inLine, commandPattern)) {
271: // extract key and value and insert them into
272: // the reference pattern list
273: MatchResult result = matcher.getMatch();
274: String key = result.group(1);
275: String value = result.group(2);
276: L10nKey l10nKey = new L10nKey(key, value, lineNo);
277: if (value.indexOf('{') >= 0) {
278: // handle attributes
279: try {
280: MessageFormat fmt = new MessageFormat(value);
281: int attributeCount = fmt.getFormats().length;
282: l10nKey.setAttributeCount(attributeCount);
283: } catch (IllegalArgumentException exIAE) {
284: addMessage(lineNo, new IllegalPatternIssue(
285: key), null);
286: continue;
287: }
288: }
289: l10nKey.setNoTrans(doNotTrans);
290: doNotTrans = false;
291:
292: // we've seen this key
293: if (seen.contains(l10nKey)) {
294: // same entry in translation twice
295: L10nKey orig = (L10nKey) seen.get(l10nKey);
296: if (orig.getValue().equals(l10nKey.getValue())) {
297: // same entry with same translation -> info
298: addMessage(lineNo,
299: new TranslatedTwiceIssue(key, orig
300: .getLineNo()), l10nKey);
301: } else {
302: // same key, different translation -> error
303: addMessage(lineNo,
304: new TranslatedTwiceDiffIssue(key,
305: orig.getLineNo()), l10nKey);
306: }
307: seen.remove(orig); // remove original key
308: seen.put(l10nKey, l10nKey);
309: continue; // do not create other errors here
310: }
311: seen.put(l10nKey, l10nKey);
312: L10nKey ref = (L10nKey) refProperties.get(key);
313: if (ref == null) {
314: // error: key not in reference
315: addMessage(lineNo,
316: new NotInReferenceIssue(key), l10nKey);
317: } else {
318: if (ref.isNoTrans()) {
319: // This entry is not supposed to be translated
320: addMessage(lineNo, new NoTransAllowedIssue(
321: key), l10nKey);
322: } else if (ref.getValue().equals(value)) {
323: if (ref.isNeedTrans()) {
324: // info: Key not found in translation set. but required
325: addMessage(lineNo,
326: new TranslationRequiredIssue(
327: key), ref);
328: } else {
329: // not translated. Will only add the warning if the
330: // key has not been marked as NOTRANS in the target language
331: // bundle.
332: if (!l10nKey.isNoTrans())
333: addMessage(
334: lineNo,
335: new NotTranslatedIssue(key),
336: l10nKey);
337: }
338: } else if (ref.getAttributeCount() != l10nKey
339: .getAttributeCount()) {
340: // different number of attributes in reference and translation
341: addMessage(
342: lineNo,
343: new DifferentAttributeCountIssue(
344: key,
345: l10nKey.getAttributeCount(),
346: ref.getAttributeCount()),
347: l10nKey);
348: }
349: }
350: } else {
351: addMessage(lineNo, new CantParseLineIssue(inLine),
352: null);
353: }
354: }
355: } catch (IOException exIO) {
356: Log.get().error(exIO);
357: exIO.printStackTrace();
358: // cleanup resources
359: refProperties.clear();
360: messages.clear();
361: throw exIO; // rethrow
362: } catch (Exception e) {
363: Log.get().error(e);
364: e.printStackTrace();
365: // cleanup resources
366: refProperties.clear();
367: messages.clear();
368: throw new IOException(e.getMessage());
369: }
370:
371: // look for missing messages
372: Iterator it = refProperties.keySet().iterator();
373: while (it.hasNext()) {
374: String key = (String) it.next();
375: L10nKey refKey = (L10nKey) refProperties.get(key);
376: if (!seen.contains(refKey)) {
377: if (refKey.isNeedTrans()) {
378: // info: Key not found in translation set. but required
379: addMessage(-1, new TranslationRequiredIssue(key),
380: refKey);
381: } else {
382: // info: Key not found in translation set
383: addMessage(-1, new TranslationMissingIssue(key),
384: refKey);
385: }
386: }
387: }
388: this .linesRead = lineNo;
389: return seen.size();
390: }
391:
392: /* private functions */
393:
394: /**
395: * Load a the reference file.
396: *
397: * <p>
398: * TODO: Sophisticated exception handling
399: */
400: private void loadReferenceFile(File inFile) throws IOException {
401: BufferedReader inStream = null;
402: String inLine;
403: int lineNo = 0;
404: boolean transNeeded = false;
405: boolean doNotTrans = false;
406:
407: try {
408: inStream = new BufferedReader(new FileReader(inFile));
409: while ((inLine = inStream.readLine()) != null) {
410: lineNo++;
411: if (matcher.matches(inLine, transPattern)) {
412: MatchResult result = matcher.getMatch();
413: if (result.group(1).equals("+")) {
414: transNeeded = true;
415: } else {
416: doNotTrans = true;
417: }
418: } else if (matcher.matches(inLine, commentPattern)) {
419: // skip comment lines
420: continue;
421: } else if (matcher.contains(inLine, commandPattern)) {
422: // extract key and value and insert them into
423: // the reference pattern list
424: MatchResult result = matcher.getMatch();
425: String key = result.group(1);
426: String value = result.group(2);
427: L10nKey l10nKey = new L10nKey(key, value, lineNo);
428: if (value.indexOf('{') >= 0) {
429: // handle attributes
430: try {
431: MessageFormat fmt = new MessageFormat(value);
432: int attributeCount = fmt.getFormats().length;
433: l10nKey.setAttributeCount(attributeCount);
434: } catch (IllegalArgumentException exIAE) {
435: addMessage(lineNo, new IllegalPatternIssue(
436: key), null);
437: continue;
438: }
439: }
440: l10nKey.setNeedTrans(transNeeded);
441: l10nKey.setNoTrans(doNotTrans);
442: // reset values
443: transNeeded = false;
444: doNotTrans = false;
445: if (refProperties.get(key) != null) {
446: // error: key already there
447: L10nKey orig = (L10nKey) refProperties.get(key);
448: if (orig.getValue().equals(l10nKey.getValue())) {
449: addMessage(lineNo, new DefinedTwiceIssue(
450: key, orig.getLineNo()), l10nKey);
451: } else {
452: // even worse: same key with different values
453: addMessage(lineNo, new DefinedTwiceIssue(
454: key, orig.getLineNo()), l10nKey);
455: }
456: continue;
457: }
458: // add value
459: refProperties.put(key, l10nKey);
460: } else {
461: addMessage(lineNo, new CantParseLineIssue(inLine),
462: null);
463: }
464: }
465: } catch (IOException exIO) {
466: exIO.printStackTrace();
467: Log.get().error(exIO);
468: // cleanup resources
469: refProperties.clear();
470: messages.clear();
471: throw exIO; // rethrow
472: } catch (Exception e) {
473: Log.get().error(e);
474: // cleanup resources
475: e.printStackTrace();
476: refProperties.clear();
477: messages.clear();
478: throw new IOException(e.getMessage());
479: }
480: this .linesRead = lineNo;
481: }
482:
483: /* Add an error message */
484: private void addMessage(int lineNo, L10nIssue issue, L10nKey l10nKey) {
485: L10nMessage err = new L10nMessage(lineNo, issue);
486: if (l10nKey != null) {
487: err.setL10nObject(l10nKey);
488: }
489: messages.add(err);
490: }
491:
492: /**
493: * Main line.
494: *
495: * <p>
496: * The pathes here are hardcoded, please change accordingly if you want to
497: * use the test main code
498: */
499: public static void main(String[] args) {
500: L10nInspector ins = null;
501: System.err.println("This is only used for internal tests");
502: try {
503: ins = new L10nInspector();
504: } catch (MalformedPatternException exMP) {
505: System.exit(1); // we cannot continue
506: }
507: try {
508: File f = new File(".");
509: System.err.println("We are here:" + f.getAbsolutePath());
510: ins
511: .setReference("src/conf/classes/ScarabBundle_en.properties");
512: } catch (IOException exIO) {
513: exIO.printStackTrace();
514: System.exit(1);
515: }
516: if (ins.getErrors().size() > 0) {
517: Iterator it = ins.getErrors().iterator();
518: while (it.hasNext()) {
519: L10nMessage data = (L10nMessage) it.next();
520: System.err.println("E " + data.getLineNumber() + ": "
521: + data.getMessageText());
522: }
523: }
524: if (ins.getWarnings().size() > 0) {
525: Iterator it = ins.getWarnings().iterator();
526: while (it.hasNext()) {
527: L10nMessage data = (L10nMessage) it.next();
528: System.err.println("W " + data.getLineNumber() + ": "
529: + data.getMessageText());
530: }
531: }
532: if (ins.hasErrors()) {
533: //System.exit (1);
534: }
535: System.out.println("--- checking de");
536: try {
537: int lines = ins
538: .checkFile("src/conf/classes/ScarabBundle_es.properties");
539: } catch (Exception e) {
540: e.printStackTrace();
541: System.exit(1);
542: }
543: List msgs = ins.getMessages();
544: Iterator it = msgs.iterator();
545: while (it.hasNext()) {
546: L10nMessage msg = (L10nMessage) it.next();
547: if (msg.getIssue().isError()) {
548: System.out.print('E');
549: } else if (msg.getIssue().isWarning()) {
550: System.out.print('W');
551: } else if (msg.getIssue().isInfo()) {
552: System.out.print('I');
553: }
554: if (msg.getLineNumber() > 0) {
555: System.out.print(" " + msg.getLineNumber());
556: }
557: System.out.println(": " + msg.getMessageText());
558: }
559: }
560: }
|