Source code: jsd/ftp/tpl/Template.java
1 /*
2 * ----------------------------------------------------------------------------
3 * JStrangeDownloader and all accompanying source code files are
4 * Copyright (C) 2002 Dusty Davidson (dustyd@iastate.edu)
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 * ----------------------------------------------------------------------------
20 *
21 * Please see gpl.txt for the full text of the GNU General Public
22 * License.
23 */
24
25 package jsd.ftp.tpl;
26
27 import java.io.*;
28 import java.util.*;
29
30
31 /**
32 * This class is used to load files into <code>OutputStream</code>
33 * after parsing the template file. It supports variables,
34 * for block, if-then-else block and iterator block. It has
35 * only one public method - <code>loadFile(OutputStream, Map)</code>.
36 * It also supports vector indexing. It stores the file data in a
37 * byte array. We may face some problems later due to character
38 * encoding. But for the time being it works fine.
39 * <a href="syntax.html">Template syntax.</a>
40 *
41 * @author <a href="mailto:rana_b@yahoo.com">Rana Bhattacharyya</a>
42 */
43 public
44 class Template {
45
46 // whitespaces
47 private static final String mstWhitespaces = " \t\r\n\f";
48
49 // keywords
50 private static final String[] mstKeywords = { "IF",
51 "FOR",
52 "ITR",
53 "VARKEY",
54 "NULL"
55 };
56
57 // keyword index
58 private static final int IF = 0;
59 private static final int FOR = 1;
60 private static final int ITR = 2;
61 private static final int VARKEY = 3;
62 private static final int NULL = 4;
63 private static final int VAR = 5;
64
65 private static final String SIZE = ".size";
66 private static final String LAST = ".last";
67 private static final String THRU = "THRU";
68 private static final String EQ = "==";
69 private static final String NE = "!=";
70 private static final String IN = "IN";
71
72 private File mFile;
73 private long mModifiedTime = 0;
74 private byte[] mbyContent;
75
76
77 /**
78 * Constructor.
79 */
80 public Template(File file) {
81 mFile = file;
82 }
83
84
85 /**
86 * Read file (if modified) into a byte array.
87 * This function is <code>Synchronized</code> to make
88 * this class thread safe.
89 */
90 private synchronized void readFile() throws IOException {
91
92 // file check
93 if (!mFile.exists())
94 throw new IOException(toString() + " : does not exist.");
95 if (!mFile.canRead())
96 throw new IOException(toString() + " : no read permission.");
97 if (!mFile.isFile())
98 throw new IOException(toString() + " : not a file.");
99
100 // file modified
101 boolean bNeeded = false;
102 long modTime = mFile.lastModified();
103 if (modTime > mModifiedTime) {
104 mModifiedTime = modTime;
105 bNeeded = true;
106 }
107
108 if (bNeeded) {
109 mbyContent = new byte[(int)mFile.length()];
110 FileInputStream fis = new FileInputStream(mFile);
111 fis.read(mbyContent);
112 fis.close();
113 }
114 }
115
116
117 /**
118 * Process block. First find the block type and then process.
119 */
120 private int processBlock(OutputStream out, Map hash, Block block)
121 throws IOException {
122
123 // get keyword type or var
124 int type = VAR;
125 for (int i=0; i<mstKeywords.length; i++) {
126 if ( block.equals(mstKeywords[i]) ) {
127 type = i;
128 break;
129 }
130 }
131
132 return processBlock(out, hash, block, type);
133 }
134
135
136 /**
137 * Process block - call respective functions. Each of these
138 * functions returns the next index from where the next
139 * processing starts.
140 */
141 private int processBlock(OutputStream out, Map hash,
142 Block block, int type)
143 throws IOException {
144
145 int index;
146
147 switch (type) {
148
149 case VAR:
150 index = processVarBlock(out, hash, block);
151 break;
152
153 case IF:
154 index = processIfBlock(out, hash, block.nextIndex());
155 break;
156
157 case FOR:
158 index = processForBlock(out, hash, block.nextIndex());
159 break;
160
161 case VARKEY:
162 index = processVarkeyBlock(out, block);
163 break;
164
165 case NULL:
166 index = processNullBlock(out, block);
167 break;
168
169 case ITR:
170 index = processItrBlock(out, hash, block.nextIndex());
171 break;
172
173 default:
174 throw new IOException("Unknown keyword index=" + type);
175 }
176
177 return index;
178 }
179
180
181 /**
182 * Process null block - returns nothing
183 */
184 private int processNullBlock(OutputStream out, Block block) {
185 return block.nextIndex();
186 }
187
188
189 /**
190 * Process varkey.
191 * It is used to write ${ in the <code>OutputStream</code>.
192 */
193 private int processVarkeyBlock(OutputStream out, Block block)
194 throws IOException {
195
196 out.write('$');
197 out.write('{');
198 return block.nextIndex();
199 }
200
201
202 /**
203 * Process normal block. Normal block may contain other variables,
204 * keywords and other normal blocks. Search for "${". If found,
205 * get the block and process it.
206 */
207 private int processNormalBlock(OutputStream out, Map hash, Block block)
208 throws IOException {
209
210 int ind = block.miStart;
211 int finalIndex = ind + block.miLength;
212 char c, c1;
213 while (ind < finalIndex) {
214 c = (char)mbyContent[ind++];
215
216 if (c == '$') {
217 c1 = (char)mbyContent[ind++];
218
219 // not a variable
220 if (c1 != '{') {
221 out.write(c);
222 out.write(c1);
223 continue;
224 }
225
226 // variable and/or keyword
227 Block nextBlock = getBlock(ind);
228 ind = processBlock(out, hash, nextBlock);
229 } else {
230 out.write(c);
231 }
232 }
233
234 return block.nextIndex();
235 }
236
237
238 /**
239 * Process variable block. Get variable object from the
240 * <code>Map</code> and write the string representation
241 * of the object.
242 */
243 private int processVarBlock(OutputStream out, Map hash, Block block)
244 throws IOException {
245
246 // get variable value and send it
247 String sb = block.toString();
248 Object val = getVarObject(hash, sb);
249
250 if (val != null)
251 out.write(val.toString().getBytes());
252
253 return block.nextIndex();
254 }
255
256
257 /**
258 * Get variable object.
259 * Returns null if not available in the Map
260 */
261 private Object getVarObject(Map hash, String var)
262 throws IOException {
263
264 // get actual variable string
265 String sb = getActualVarString(hash, var);
266
267 // get vector size
268 if (sb.endsWith(SIZE)) {
269 return getVarSize(hash, var);
270 }
271
272 // get last element of a vector
273 if (sb.endsWith(LAST)) {
274 return getLastVar(hash, var);
275 }
276
277 // get vector element at a particular index
278 int startIndex = sb.indexOf('[');
279 int endIndex = sb.indexOf(']');
280 if (startIndex != -1 && endIndex != -1) {
281 if (endIndex < endIndex)
282 throw new IOException("List indexing error");
283
284 String indexBlock = sb.substring(startIndex+1, endIndex);
285 String vectVar = sb.substring(0, startIndex);
286 Object obj = getVarObject(hash, vectVar);
287 return getIndexedObject(obj, indexBlock);
288 }
289
290 // null variable
291 if (sb.equals(mstKeywords[NULL])) {
292 return null;
293 }
294
295 // return other values
296 return hash.get(sb);
297 }
298
299
300 /**
301 * Get conditional variable object. If not a variable returns
302 * the string itself. Else returns the equivalent object from
303 * the hashtable. If it is not available in the hashtable
304 * returns null.
305 */
306 private Object getCondVarObject(Map hash, String str)
307 throws IOException{
308
309 Object obj = null;
310
311 // variable
312 if (str.charAt(0) == '$' && str.charAt(1) == '{') {
313 String var1 = str.substring(2, str.length() - 1);
314 String var2 = getBlock(str, 2);
315
316 if (var1.equals(var2))
317 obj = getVarObject(hash, var1);
318 else
319 obj = getActualVarString(hash, str);
320 } else
321 obj = getActualVarString(hash, str);
322
323 return obj;
324 }
325
326
327 /**
328 * Returns the size of a variable as an <code>Integer</code>
329 * object. In case of Scalar returns 1. If object is null,
330 * returns 0 and in case of <code>List</code>
331 * returns the list size.
332 */
333 private Integer getVarSize(Map hash, String sb)
334 throws IOException {
335
336 // get object form hashtable
337 String vectName = sb.substring(0, sb.length()-SIZE.length());
338 Object obj = getVarObject(hash, vectName);
339
340 if (obj == null)
341 return new Integer(0);
342
343 if (obj instanceof java.util.List)
344 return new Integer(((List)obj).size());
345
346 return new Integer(1);
347 }
348
349
350 /**
351 * Returns the last element of a vector. In case of scalar,
352 * returns the object itself. In case of null returns null
353 * and in case of vector returns null if size is zero or the
354 * last element.
355 */
356 private Object getLastVar(Map hash, String sb)
357 throws IOException {
358
359 String vectName = sb.substring(0, sb.length()-LAST.length());
360 Object obj = getVarObject(hash, vectName);
361
362 if (obj == null)
363 return null;
364
365 if (obj instanceof java.util.List) {
366 List v = (List)obj;
367 if (v.size() == 0)
368 return null;
369 else
370 return v.get(v.size() - 1);
371 }
372
373 return obj;
374 }
375
376
377 /**
378 * Get vector element at a particular index. Tokenize
379 * <code>indexBlock</code> string and returns the element.
380 */
381 private Object getIndexedObject(Object obj, String indexBlock) {
382
383 if (obj == null)
384 return null;
385
386 StringTokenizer st = new StringTokenizer(indexBlock, ",");
387 while (st.hasMoreTokens()) {
388 String initVar = st.nextToken().trim();
389 int ind = Integer.parseInt(initVar);
390 obj = elementAt(obj, ind);
391 }
392
393 st = null;
394 return obj;
395 }
396
397
398 /**
399 * Returns an element at a particular element. If the object
400 * is null, returns null. If the object is a <code>List</code>
401 * returns null if the index is out of range or the element at
402 * that index.
403 */
404 private Object elementAt(Object obj, int index) {
405
406 if (obj == null)
407 return null;
408
409 if (obj instanceof java.util.List) {
410
411 int size = ((List)obj).size();
412 if (index >= size || index < 0)
413 return null;
414
415 return((List)obj).get(index);
416 }
417
418 if (index == 0)
419 return obj;
420
421 return null;
422 }
423
424
425 /**
426 * Variable name can be formed dynamically. This function is
427 * used to get the actual variable name - by resolving inner
428 * variable name. Keywords will be treated as variables. The
429 * variable within '{' and '}' will be replaced by its string
430 * representation.
431 */
432 private String getActualVarString(Map hash, String initVar)
433 throws IOException {
434
435 int ind = 0;
436 char c, c1;
437 int size = initVar.length();
438 StringBuffer sb = new StringBuffer(64);
439
440 while (ind < size) {
441 c = initVar.charAt(ind);
442 ++ind;
443
444 if (c == '$') {
445 c1 = initVar.charAt(ind);
446 ind++;
447
448 // not a variable
449 if (c1 != '{') {
450 sb.append(c);
451 sb.append(c1);
452 continue;
453 }
454
455 // variable and/or keyword
456 String var = getBlock(initVar, ind);
457 ind += var.length() + 1;
458 Object obj = getVarObject(hash, var);
459 if (obj != null)
460 sb.append(obj);
461 } else {
462 sb.append(c);
463 }
464 }
465
466 return sb.toString();
467 }
468
469
470 /**
471 * Process IF block. Returns the index of the next char.
472 */
473 private int processIfBlock(OutputStream out, Map hash, int start)
474 throws IOException {
475
476 // condition block starts - evaluate condition
477 int index = ignoreWhitespace(start);
478 if (mbyContent[index] != '{')
479 throw new IOException("IF condition block not found.");
480
481 Block block = getBlock(index + 1);
482 boolean bTrue = isTrueCondition(hash, block);
483
484 // get IF action block
485 index = block.nextIndex();
486 index = ignoreWhitespace(index);
487 if (mbyContent[index] != '{')
488 throw new IOException("Invalid char after IF");
489 block = getBlock(index + 1);
490 if (bTrue)
491 processNormalBlock(out, hash, block);
492
493
494 // ignore ELSE block if available and condition is TRUE.
495 index = block.nextIndex();
496 char c = (char)mbyContent[index];
497 if (bTrue) {
498 if (c == '{') {
499 block = getBlock(index + 1);
500 return block.nextIndex();
501 }
502 }
503
504 // else block starts
505 if (!bTrue) {
506
507 // check ELSE block
508 if (c == '{') {
509 block = getBlock(index + 1);
510 processNormalBlock(out, hash, block);
511 return block.nextIndex();
512 }
513 }
514
515 return index;
516 }
517
518
519 /**
520 * Evaluate condition block in IF statement.
521 */
522 private boolean isTrueCondition(Map hash, Block block)
523 throws IOException {
524
525 String sb = block.toString();
526 StringTokenizer st = new StringTokenizer(sb, mstWhitespaces);
527
528 // get left side
529 if (!st.hasMoreTokens())
530 throw new IOException("First token not found in IF");
531 Object leftObj = getCondVarObject(hash, st.nextToken());
532
533 // get condition operator
534 if (!st.hasMoreTokens())
535 throw new IOException("IF condition operator not found");
536 String condition = st.nextToken();
537
538 // get rightside
539 if (!st.hasMoreTokens())
540 throw new IOException("Last token not found in IF");
541 Object rightObj = getCondVarObject(hash, st.nextToken());
542
543 // now evaluate
544 if (leftObj == null && rightObj == null) return condition.equals(EQ);
545 if (leftObj == null || rightObj == null) return condition.equals(NE);
546 if (condition.equals(EQ)) return rightObj.toString().equals(leftObj.toString());
547 if (condition.equals(NE)) return !rightObj.toString().equals(leftObj.toString());
548 if (condition.equals(IN)) return ifObjExists(leftObj, rightObj);
549
550 throw new IOException("Invalid IF condition operator: " + condition);
551 }
552
553
554 /**
555 * Check object existance. If any vector element is null,
556 * that element is ignored.
557 */
558 private boolean ifObjExists(Object left, Object right) {
559
560 // vector
561 if (right instanceof java.util.List) {
562 int sz = ((List)right).size();
563 for (int i=0; i<sz; i++) {
564 Object obj = ((List)right).get(i);
565 if (obj == null)
566 continue;
567 if (obj.toString().equals(left.toString()))
568 return true;
569
570 }
571 return false;
572 }
573
574 // not a vector
575 return right.toString().equals(left.toString());
576 }
577
578
579 /**
580 * Process iterator block. Here <code>index</code> is the
581 * starting index of the iterator initialization block.
582 */
583 private int processItrBlock(OutputStream out, Map hash, int index)
584 throws IOException {
585
586 int ind = ignoreWhitespace(index);
587 if (mbyContent[ind] != '{')
588 throw new IOException("ITR initialization block not found.");
589
590 // get ITR init block
591 Block sb = getBlock(ind + 1);
592 String str = sb.toString();
593 StringTokenizer st = new StringTokenizer(str, mstWhitespaces);
594
595 // get ITR main block
596 ind = ignoreWhitespace(sb.nextIndex());
597 if (mbyContent[ind] != '{')
598 throw new IOException("ITR main block not found.");
599 sb = getBlock(ind + 1);
600 int retValue = sb.nextIndex();
601
602
603 // get init variable name
604 if (!st.hasMoreTokens())
605 throw new IOException("Variable name not found in ITR block");
606 String var = st.nextToken();
607
608 // get start point
609 int start = getIntegerValue(hash, st);
610 if (start == -1)
611 return retValue;
612
613 // strip "THRU"
614 if (!st.hasMoreTokens())
615 throw new IOException("Invalid ITR block - string THRU not found");
616 if (!st.nextToken().equals(THRU))
617 throw new IOException("Expecting THRU in ITR block");
618
619 // get length
620 int len = getIntegerValue(hash, st);
621 if (len == -1)
622 return retValue;
623
624 // process ITR main loop
625 for (int i=0; i<len; i++) {
626 hash.put(var, new Integer(i+start));
627 processNormalBlock(out, hash, sb);
628 }
629
630 return retValue;
631 }
632
633
634 /**
635 * Get the integer value from <code>StringTokenizer</code>.
636 * This function is used to get the start index and iteration
637 * count in ITR block. It returns -1 in case of error
638 * (<code>NumberFormatException</code>).
639 */
640 private int getIntegerValue(Map hash, StringTokenizer st)
641 throws IOException {
642
643 // no tokens available
644 if (!st.hasMoreTokens())
645 throw new IOException("Start index not found in ITR block");
646
647 String initVar = st.nextToken();
648
649 // Get string representation of the number
650 String initVal = getActualVarString(hash, initVar);
651 try {
652 return Integer.parseInt(initVal);
653 } catch (NumberFormatException ex) {
654 return -1;
655 }
656 }
657
658
659 /**
660 * Process FOR block. Here <code>index</code> is the
661 * start index of the FOR initialization block.
662 */
663 private int processForBlock(OutputStream out, Map hash, int index)
664 throws IOException {
665
666 // read initialization block
667 int ind = ignoreWhitespace(index);
668 char c = (char)mbyContent[ind];
669 if (c != '{')
670 throw new IOException("FOR initialization block not found.");
671
672 Block sb = getBlock(ind + 1);
673 StringBuffer varNameSb = new StringBuffer();
674 List vec = getForList(hash, varNameSb, sb);
675 String varName = varNameSb.toString();
676
677
678 // read for main loop
679 ind = ignoreWhitespace(sb.nextIndex());
680 c = (char)mbyContent[ind];
681 if (c != '{')
682 throw new IOException("FOR main loop not found.");
683
684 sb = getBlock(ind + 1);
685 int sz = vec.size();
686 for (int i=0; i<sz; i++) {
687 Object ob = vec.get(i);
688 hash.put(varName, ob);
689 processNormalBlock(out, hash, sb);
690 }
691
692 return sb.nextIndex();
693 }
694
695
696 /**
697 * Get for vector and init variable name. It passes the
698 * name of the FOR temporary variable name in
699 * <code>StringBuffer sb</code>
700 */
701 private List getForList(Map hash,
702 StringBuffer sb,
703 Block block) throws IOException {
704
705 String initBlock = block.toString();
706 StringTokenizer st = new StringTokenizer(initBlock, mstWhitespaces);
707
708 // get init variable
709 if (!st.hasMoreTokens())
710 throw new IOException("Invalid FOR block");
711 String initVar = st.nextToken();
712 sb.append(initVar);
713
714
715 // ignore string "IN"
716 if (!st.hasMoreTokens())
717 throw new IOException("Invalid FOR block - string IN not found");
718 initVar = st.nextToken();
719 if (!initVar.equals(IN))
720 throw new IOException("Expecting IN, found " + initVar);
721
722
723 // get vector variable
724 if (!st.hasMoreTokens())
725 throw new IOException("Invalid FOR block - vector not found");
726 initVar = st.nextToken();
727
728
729 // get object form Map if variable
730 Object obj = getCondVarObject(hash, initVar);
731
732 if (obj instanceof java.util.List)
733 return(List)obj;
734
735 List vec = new Vector(1);
736 if (obj != null)
737 vec.add(obj);
738
739 return vec;
740 }
741
742
743 /**
744 * Get block from index <code>start</code>.
745 * It starts just after the '{' character.
746 * It reads till it reaches the corresponding '}'.
747 */
748 private Block getBlock(int start) {
749
750 char c;
751 int braceCount = 0;
752 int index = start;
753
754 while (braceCount != 1) {
755 c = (char)mbyContent[index++];
756
757 if (c == '{')
758 --braceCount;
759 else if (c == '}')
760 ++braceCount;
761 }
762
763 return new Block(mbyContent, start, index - start - 1);
764 }
765
766
767 /**
768 * Get block - this function is used to form the
769 * variable name dynamically.
770 */
771 private String getBlock(String str, int start) {
772
773 char c;
774 int braceCount = 0;
775 int index = start;
776
777 while (braceCount != 1) {
778 c = str.charAt(index++);
779
780 if (c == '{')
781 --braceCount;
782 else if (c == '}')
783 ++braceCount;
784 }
785
786 return str.substring(start, index-1);
787 }
788
789
790 /**
791 * Ignore whitespace. Returns the first
792 * non-whitespace char index.
793 */
794 private int ignoreWhitespace(int start) {
795
796 char c;
797 while (true) {
798 c = (char)mbyContent[start];
799
800 if (mstWhitespaces.indexOf(c) != -1) {
801 ++start;
802 continue;
803 } else
804 break;
805 }
806 return start;
807 }
808
809
810 /**
811 * Load file - it reads the file and process the block as
812 * normal block.
813 */
814 public void loadFile(OutputStream out, Map hash)
815 throws IOException {
816
817 try {
818 readFile();
819 Block block = new Block(mbyContent);
820 processNormalBlock(out, hash, block);
821 } catch (ArrayIndexOutOfBoundsException ex) {
822 throw new IOException("Unexpected end of file - bracket mismatch.");
823 } catch (Throwable th) {
824 throw new IOException(th.getLocalizedMessage());
825 }
826 }
827
828 /**
829 * Get template file name
830 */
831 public String toString() {
832 return mFile.getAbsolutePath();
833 }
834
835 }