tema
 
(Mikhail Kryshen)
2006-05-16: Tema 0.1 (imported from CVS). release_0_1

Tema 0.1 (imported from CVS).

diff --git a/build.xml b/build.xml
new file mode 100644
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<project name="tema" default="dist" basedir=".">
+
+    <property name="src"      value="src"/>
+    <property name="build"    value="build"/>
+    <property name="dist"     value="dist"/>
+    <property name="res"      value="res"/>
+    <property name="jar_file" value="tema.jar"/>
+
+    <target name="init">
+	<tstamp/>
+	<mkdir dir="${build}"/>
+    </target>
+
+    <target name="compile" depends="init">
+        <javac srcdir="${src}" destdir="${build}"
+               deprecation="on" optimize="on" debug="on"/>
+    </target>
+
+    <target name="dist" depends="compile">
+	<jar jarfile="${dist}/${jar_file}" manifest="${src}/Manifest.mf">
+	    <fileset dir="${build}" includes="**/*.class"/>
+	</jar>
+    </target>    
+
+    <target name="clean">
+	<delete>
+	    <fileset dir="${build}" includes="**/*.class"/>
+	</delete>
+	<delete file="${dist}/${jar_file}"/>
+    </target>
+    
+</project>
diff --git a/dist/biotopes/biotope-top.sql b/dist/biotopes/biotope-top.sql
new file mode 100644
--- /dev/null
+++ b/dist/biotopes/biotope-top.sql
@@ -0,0 +1,20 @@
+SELECT t1.*,
+
+(SELECT t2.rusname FROM biotopelist AS t2
+WHERE Left(t2.rangcode, 1) = Left(t1.rangcode, 1) AND
+Right(t2.rangcode, 8) = '00000000') AS class0,
+
+(SELECT t2.rusname FROM biotopelist AS t2
+WHERE Left(t2.rangcode, 3) = Left(t1.rangcode, 3) AND
+Right(t2.rangcode, 6) = '000000') AS class1,
+
+(SELECT t2.rusname FROM biotopelist AS t2
+WHERE Left(t2.rangcode, 5) = Left(t1.rangcode, 5) AND
+Right(t2.rangcode, 4) = '0000') AS class2,
+
+(SELECT t2.rusname FROM biotopelist AS t2
+WHERE Left(t2.rangcode, 7) = Left(t1.rangcode, 7) AND
+Right(t2.rangcode, 2) = '00') AS class3
+
+FROM biotopelist AS t1
+WHERE Right(t1.rangcode, 2) <> '00' AND rusname
diff --git a/dist/biotopes/biotope.sql b/dist/biotopes/biotope.sql
new file mode 100644
--- /dev/null
+++ b/dist/biotopes/biotope.sql
@@ -0,0 +1,24 @@
+SELECT t1.*,
+
+(SELECT t2.rusname FROM biotopelist AS t2
+WHERE Left(t2.rangcode, 1) = Left(t1.rangcode, 1)
+AND Right(t2.rangcode, 8)  = '00000000'
+AND Right(t1.rangcode, 8) <> '00000000') AS class0,
+
+(SELECT t2.rusname FROM biotopelist AS t2
+WHERE Left(t2.rangcode, 3) = Left(t1.rangcode, 3)
+AND Right(t2.rangcode, 6)  = '000000'
+AND Right(t1.rangcode, 6) <> '000000') AS class1,
+
+(SELECT t2.rusname FROM biotopelist AS t2
+WHERE Left(t2.rangcode, 5) = Left(t1.rangcode, 5)
+AND Right(t2.rangcode, 4)  = '0000'
+AND Right(t1.rangcode, 4) <> '0000') AS class2,
+
+(SELECT t2.rusname FROM biotopelist AS t2
+WHERE Left(t2.rangcode, 7) = Left(t1.rangcode, 7)
+AND Right(t2.rangcode, 2)  = '00'
+AND Right(t1.rangcode, 2) <> '00') AS class3
+
+FROM biotopelist AS t1
+WHERE rusname
diff --git a/dist/biotopes/biotope.template b/dist/biotopes/biotope.template
new file mode 100644
--- /dev/null
+++ b/dist/biotopes/biotope.template
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="ISO-8859-5"?>
+<!DOCTYPE _ SYSTEM "brief.dtd">
+<!-- rangcode: <%xml_escape get\rangcode%> -->
+<_ show="0">
+  < display="0">
+    <%optional:<_ ="1" file="<%xml_escape image:maps/<%get\rangcode%>.gif maps/<%get\code%>.png png%>"/>%>
+<%query:photo_sql [%\<%include\photo.template%>%] <%get\code%>%>
+  </>
+  <><%xml_escape get\rusname%></>
+  <><%xml_escape get\class0%></>
+  <1><%xml_escape get\class1%></1>
+  <2><%xml_escape get\class2%></2>
+  <3><%xml_escape get\class3%></3>
+  <_ rows="3"><%xml_cdata get\descript%></_>
+  <_ rows="3"><%xml_cdata get\geobotdescr%></_>
+  <_><%xml_escape get\contributors%></_>
+</_>
diff --git a/dist/biotopes/brief.dtd b/dist/biotopes/brief.dtd
new file mode 100644
--- /dev/null
+++ b/dist/biotopes/brief.dtd
@@ -0,0 +1,27 @@
+<?xml encoding="ISO-8859-5"?>
+<!ELEMENT _ (?, , , 1, 2, 3, _, _, _)>
+<!ELEMENT  (|_)*>
+<!ELEMENT  EMPTY>
+<!ELEMENT _ EMPTY>
+<!ELEMENT  (#PCDATA)>
+<!ELEMENT  (#PCDATA)>
+<!ELEMENT 1 (#PCDATA)>
+<!ELEMENT 2 (#PCDATA)>
+<!ELEMENT 3 (#PCDATA)>
+<!ELEMENT _ (#PCDATA)>
+<!ELEMENT _ (#PCDATA)>
+<!ELEMENT _ (#PCDATA)>
+<!ATTLIST _ show (0|1) "1"> 
+<!ATTLIST 
+	display CDATA #FIXED "0">
+<!ATTLIST 
+	num ID #REQUIRED
+	file CDATA #REQUIRED
+	big CDATA #REQUIRED
+	text CDATA #IMPLIED
+	 CDATA #IMPLIED>
+<!ATTLIST _
+	num ID #REQUIRED
+	file CDATA #REQUIRED>
+<!ATTLIST _ rows CDATA #FIXED "3">
+<!ATTLIST _ rows CDATA #FIXED "3">
diff --git a/dist/biotopes/class.sql b/dist/biotopes/class.sql
new file mode 100644
--- /dev/null
+++ b/dist/biotopes/class.sql
@@ -0,0 +1,7 @@
+SELECT t1.*
+FROM biotopelist AS t1
+WHERE rusname AND Right(t1.rangcode, 8)  = '00000000'
+  AND EXISTS
+    (SELECT NULL FROM biotopelist AS t2
+     WHERE Left(t2.rangcode, 1) = Left(t1.rangcode, 1)
+     AND Right(t2.rangcode, 8) <> '00000000')
diff --git a/dist/biotopes/classes.template b/dist/biotopes/classes.template
new file mode 100644
--- /dev/null
+++ b/dist/biotopes/classes.template
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="ISO-8859-5"?>
+<><%query:class_sql [%\
+  <!-- rangcode: <%xml_escape get\rangcode%> -->
+  < id="<%get\NUMBER%>"><%xml_escape get\rusname%></>%]%>
+</>
diff --git a/dist/biotopes/main.template b/dist/biotopes/main.template
new file mode 100644
--- /dev/null
+++ b/dist/biotopes/main.template
@@ -0,0 +1,11 @@
+<%!:
+  <%prepare:biotope_sql <%read\biotope.sql%>%>
+  <%prepare:photo_sql <%read\photo.sql%>%>
+  <%prepare:class_sql <%read\class.sql%>%>
+  <%prepare:plant_sql <%read\plant.sql%>%>
+
+  <%query:biotope_sql [%\<%write:<%get\code%>.xml <%include\biotope.template%>%> %]%>
+
+  <%write:classes.xml <%include\classes.template%>%>
+  <%write:plants.xml <%include\plants.template%>%>
+%>
diff --git a/dist/biotopes/photo.sql b/dist/biotopes/photo.sql
new file mode 100644
--- /dev/null
+++ b/dist/biotopes/photo.sql
@@ -0,0 +1,1 @@
+SELECT * from photos WHERE biotope = ?
diff --git a/dist/biotopes/photo.template b/dist/biotopes/photo.template
new file mode 100644
--- /dev/null
+++ b/dist/biotopes/photo.template
@@ -0,0 +1,3 @@
+    < num="<%xml_escape get\NUMBER%>" <%optional:="<%xml_escape get\authphoto%>" %>text="" 
+                file="<%xml_escape image:biotopephotos/<%get\photo%> images/<%get\SUPER.code%>_<%get\NUMBER%>.jpg jpg 300 300%>" 
+                big="<%xml_escape copy:biotopephotos/<%get\photo%> images/big/<%get\SUPER.code%>_<%get\NUMBER%>.jpg%>"/>
diff --git a/dist/biotopes/plant.sql b/dist/biotopes/plant.sql
new file mode 100644
--- /dev/null
+++ b/dist/biotopes/plant.sql
@@ -0,0 +1,3 @@
+SELECT t1.*
+FROM biotopelist AS t1
+WHERE rusname AND Right(t1.rangcode, 2) <> '00'
diff --git a/dist/biotopes/plants.template b/dist/biotopes/plants.template
new file mode 100644
--- /dev/null
+++ b/dist/biotopes/plants.template
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="ISO-8859-5"?>
+<><%query:plant_sql [%\
+  <!-- rangcode: <%xml_escape get\rangcode%> -->
+  <><%xml_escape get\rusname%></>%]%>
+</>
diff --git a/dist/biotopes/tema.properties b/dist/biotopes/tema.properties
new file mode 100644
--- /dev/null
+++ b/dist/biotopes/tema.properties
@@ -0,0 +1,22 @@
+# Data source configuration
+resource          : jdbc:odbc:biotopes-data
+driver            : sun.jdbc.odbc.JdbcOdbcDriver
+
+# Base directory for images and files
+resource_base     : C:\\biotopes\\db
+
+# Template to start processing with
+main_template     : main.template
+
+# File encodings
+input_encoding    : ISO-8859-5
+output_encoding   : ISO-8859-5
+
+# Cache templates
+cache_read        : true
+
+# Output main_template parsing result to stderr
+#output            : stderr
+
+# File to output error messages (redirect stderr)
+#log               : tema.log
diff --git a/dist/readme.html b/dist/readme.html
new file mode 100644
--- /dev/null
+++ b/dist/readme.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
+          "DTD/xhtml1-transitional.dtd">
+
+<html>
+
+<head>
+  <title>DbReader</title>
+  <meta http-equiv="Content-Type" content="text/html; charset=koi8-r" />
+</head>
+
+<body>
+<h1>DbReader</h1>
+
+<h2> </h2>
+<ol>
+<li> .</li>
+
+<li>  :<br />
+ Windows 2000:  //  (ODBC),  biotopes-data.mdb.</li>
+
+<li> :<br />
+  dbreader.properties  ,   :<br />
+    <code>resource: jdbc:odbc:<em></em></code><br />
+   ,       ( biotopephotos  maps):<br />
+    <code>resource_base: C:\\biotopes</code><br />
+( '\'  )</li>
+
+<li> :<br />
+<code>dbreader.bat</code> <br />
+<code>java -jar dbreader.jar</code>.<br />
+   Java Runtime Environment (JRE) 5.0,    <a href="http://java.sun.com/j2se/1.5.0/download.jsp">http://java.sun.com/j2se/1.5.0/download.jsp</a>. ,       JRE -  .
+
+        :<br />
+ <code>java -Dru.karrc.dbreader.<em></em>=<em></em> -jar dbreader.jar</code><br />
+(: <code>java -Dru.karrc.dbreader.log=dbreader.log -jar dbreader.jar</code>)</li>
+</ol>
+
+<h2> </h2>
+
+<p>   biotope.sql ( ,   dbreader.properties),     biotope.template,         .</p>
+<p> : <code><%<em>_</em>[:|\]<em></em>%></code>, <em> </em> -    ,    .    ,     .</p>
+
+<h3>     </h3>
+
+<ul>
+  <li><code>:</code> -    .</li>
+  <li><code>\</code> -      .       <code><%</code>  <code>%></code> (<code><%\<%%></code>  <code><%\%%>></code>).</li>
+</ul>
+
+<h3></h3>
+
+<ul>
+  <li><code><%get:<em></em>%></code><br />
+         .</li>
+
+  <li><code><%optional:<em></em>%></code><br />
+    <em></em>,     ,    ,     .</li>
+
+  <li><code><%escape:<em></em>%></code><br />
+     ""  (     ).</li>
+
+  <li><code><%invoke:<em></em>; <em>1</em> <em>2</em> ... <em>N</em>%></code><br />
+      <em></em>.sql       <em></em>.template.        '?'.</li>
+
+  <li><code><%image:<em>_</em> <em>_</em> <em></em> [<em>_</em>] [<em>_</em>]%></code><br />
+      <em>_</em> (     "resource_base")      ,    <em>_</em>.      ,    .
+  ,  <code><%image:...%></code>     <em>_</em>,  -   .</li>
+
+  <li><code><%copy:<em>_</em> <em>_</em>%></code><br />
+    <em>_</em> (     "resource_base")  <em>_</em>.   ,  <code><%copy:...%></code>     <em>_</em>,  -   .</li>
+</ul>
+
+<h3> </h3>
+
+<p><code>NUMBER</code> -      .
+    ,          invoke,      <code>SUPER.</code> (: <code>SUPER.NUMBER</code>).</p>
+
+<h3></h3>
+
+<p>      name      :</p>
+<p><code><%escape get:name%></code></p>
+
+<p align="right"><i><a href="mailto:kryshen@cs.karelia.ru"> </a></i></p>
+
+</body>
+</html>
diff --git a/dist/readme.txt b/dist/readme.txt
new file mode 100644
--- /dev/null
+++ b/dist/readme.txt
@@ -0,0 +1,63 @@
+text/plain; charset=UTF-8
+
+Установка программы
+-------------------
+
+1. Распаковать архив.
+
+2. Установить источник данных:
+в Windows 2000: Панель управления/Администрирование/Источники данных (ODBC), добавить biotopes.mdb.
+
+3. Настроить программу:
+В файле dbreader.properties указать имя, присвоенное источнику данных:
+    "resource: jdbc:odbc:<имя>"
+и путь к каталогу, относительно которого программа будет искать изображения (содержащий biotopephotos и maps):
+    "resource_base: C:\\biotopes"
+(символ '\' необходимо дублировать)
+
+4. Запуск программы: dbreader.bat или "java -jar dbreader.jar"
+Для запуска необходима Java Runtime Environment (JRE) 5.0, можно скачать с http://java.sun.com/j2se/1.5.0/download.jsp. Возможно, будет работать и со старыми версиями JRE - не проверял.
+
+Все значения файла конфигурации можно переопределять при запуске программы:
+    команда "java -Dru.karrc.dbreader.<ключ>=<значение> -jar dbreader.jar"
+(например: "java -Dru.karrc.dbreader.log=dbreader.log -jar dbreader.jar")
+
+
+Работа программы
+----------------
+
+Программа выполняет запрос biotope.sql (или другой, указанный в dbreader.properties), после чего обрабатывает файл biotope.template, заменяя найденные в нем инструкции на результаты их выполнения.
+
+Формат инструкций: "<%список_функций[:|\]данные%>", список функций - имена функций разделенные пробелом, список может быть пустым. Если указано несколько функций, они выполняются начиная с последней.
+
+Разделители между списком функций и данными:
+':' - данные будут обрабатываться рекурсивно.
+'\' - данные будут переданы функции без обработки. Можно использовать для экранирования комбинаций символов "<%" и "%>" ("<%\<%%>" и "<%\%%>>").
+
+Функции:
+<%get:<имя>%> - заменяется на значение встроенной переменной или колонки запроса.
+
+<%optional:<текст>%>
+Заменяется на <текст>, если в нем найдены инструкции, заменившиеся на непустые строки, иначе заменяется на пустую строку.
+
+<%escape:<текст>%>
+Заменяет в тексте "опасные" символы (таблица замен задается в файле конфигурации).
+
+<%invoke:<имя> <парам1> <парам2> ... <парамN>%>
+Выполняет запрос с параметрами <имя>.sql и заменяется на результаты обработки шаблона <имя>.template. Значения параметров подставляются в запрос вместо символа '?'.
+
+<%image:<исх_файл> <кон_файл> <формат> [<макс_ширина>] [<макс_высота>]%>
+Загружает изображение из файла <исх_файл> (путь определяется относительно конфигурационного параметра "resource_base") и преобразует его в указанный формат, сохраняя результат в <кон_файл>. Если заданы максимальная высота и ширина, большие изображения будут уменьшены.
+При успешном выполнении, инструкция "<%!image ...%>" будет заменена на значение <кон_файл>, иначе - на пустую строку.
+
+<%copy:<исх_файл> <кон_файл>%>
+Копирует файл <исх_файл> (путь определяется относительно конфигурационного параметра "resource_base").
+При успешном выполнении, инструкция "<%!copy ...%>" будет заменена на значение <кон_файл>, иначе - на пустую строку.
+
+Встроенные переменные:
+NUMBER - порядковый номер строки результата выполнения запроса.
+Чтобы обратиться к переменной запроса, из которого обрабатываемый шаблон был вызван с помощью функции invoke, перед именем переменной нужно добавить "SUPER." (например: "SUPER.NUMBER").
+
+Пример:
+Следующая инструкция получит значение из столбца name и заменит в нем специальные символы:
+<%escape get:name%>
diff --git a/dist/tema b/dist/tema
new file mode 100644
--- /dev/null
+++ b/dist/tema
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+java -jar tema.jar
diff --git a/dist/tema.bat b/dist/tema.bat
new file mode 100755
--- /dev/null
+++ b/dist/tema.bat
@@ -0,0 +1,1 @@
+java -jar tema.jar
diff --git a/dist/tema.properties b/dist/tema.properties
new file mode 100644
--- /dev/null
+++ b/dist/tema.properties
@@ -0,0 +1,22 @@
+# Data source configuration
+# resource          : jdbc:odbc:biotopes-data
+# driver            : sun.jdbc.odbc.JdbcOdbcDriver
+
+# Base directory for images and files
+# resource_base     : C:\\biotopes\\db
+
+# Template to start processing with
+main_template     : main.template
+
+# File encodings
+# input_encoding    : ISO-8859-5
+# output_encoding   : ISO-8859-5
+
+# Cache templates
+cache_read        : true
+
+# Output main_template parsing result to stderr
+#output            : stderr
+
+# File to output error messages (redirect stderr)
+#log               : tema.log
diff --git a/doc/article.txt b/doc/article.txt
new file mode 100644
--- /dev/null
+++ b/doc/article.txt
@@ -0,0 +1,391 @@
+*     XML-  .
+
+      " 
+"   Microsoft Access  XML- 
+       DbReader.
+
+ DbReader     
+      .  
+        
+    .   
+ SQL-,     
+.
+
+
+*  :
+
+    <%<_>{:|\}<>%>
+   
+
+<_> -   ,  .
+      .
+<> - ,  .
+
+
+*  :
+
+    [<_>][<>]
+  
+
+<_> -   ,  .
+      .
+<> - ,      .
+      .  ,   
+    ,   .
+
+      ,  
+  :
+: -  ,
+\ -   .
+
+        ,  ,
+  ,        
+   .
+
+     -  .  
+ -      .  ,
+    -    
+.
+
+  '<', '>'    '[', ']'.
+
+
+*  :
+
+:      set
+:    <>
+:        <>
+:        <>
+:    <>
+ : 1
+
+:      get
+:    <>
+:      <>. 
+ : 1,     , 0 - .
+
+:      prepare
+:    <>
+:        <>
+:      SQL- <>  ,
+                   <>.
+:    <>
+ : 1
+
+:      query
+:    <_> <> <1> ... <N>
+:        ,   
+               prepare.    
+                 '?'.    
+              ,    <>.  
+               <>    NUMBER,
+                  . ,
+                 ,   
+              <>   SUPER.<_>.
+
+:       <>   
+              .
+ :    .
+
+:      optional
+:        <>
+:    <>,      
+                  0,  -  .
+ : 1,   -  , 0 - .
+
+:      image
+:    <_> <_> <> [<_> [<_>]]
+:         <_> ( 
+                 "resource_base") 
+                  ,   
+              <_>.      , 
+                . 
+:    <_>   ,   - .
+ : 1   , 0 - .
+
+:      copy
+:    <_> <_>
+:       <_>   <_>
+	      ( <_>  
+                "resource_base").
+:    <_>   ,   - .
+ : 1   , 0 - .
+
+:      write
+:    <_>
+:        <>
+:      <>   <_>.
+:    <_>   ,   - .
+ : 1   , 0 - .
+
+:      read
+:        <_>
+:       <_>.
+:        , 
+               - .
+ : 1   , 0 - .
+
+:      include
+:        <_>
+:         <_>.
+:      .
+ :  ,    .
+
+:      !
+:        <>
+:     .
+:    .
+ :  ,     .
+
+:      replace
+:    <1> <2>
+:        <>
+:    ,     <1>
+	       <2>.
+ :  ,     .
+
+:      xml_escape
+:        <>
+:     <>,    
+              '&', '<', '>', '`', '\'   
+	       XML.
+ :  ,     .
+
+:      xml_cdata
+:        <>
+:        XML CDATA.
+ :  ,     .
+
+ DbReader :     
+,       Java.
+
+
+*   DbReader    
+  "  "   Microsoft Access 
+  XML-    
+
+       "
+ " (   ):
+
+biotopelist (code, rangcode, rusname, engname, descript, geobotdescr,
+             contributors, descrdate)
+
+  code - ,  ;
+  rangcode -    ABBCCDDEE,  , BB, CC, DD,  - 
+             ,   ;
+  rusname, engname, descript, geobotdescr -  ;
+  descrdate - .
+
+photos (biotope, photo, authphoto)
+
+  biotope - ,   code  biotopelist;
+  photo -   ;
+  authphoto - .
+
+  xml-     biotopelist, 
+  ,    ,  
+   .
+
+  xml-     
+ DbReader ( biotope.template):
+
+----------------------------------------------------------------------
+<?xml version="1.0" encoding="ISO-8859-5"?>
+<!DOCTYPE _ SYSTEM "brief.dtd">
+<!-- rangcode: <%xml_escape get\rangcode%> -->
+<_ show="0">
+  < display="0">
+    <%optional:<_ ="1"
+    file="<%xml_escape image:maps/<%get\rangcode%>.gif maps/<%get\code%>.png png%>"/>%>
+<%query:photo_sql [%\<%include\photo.template%>%] <%get\code%>%>
+  </>
+  <><%xml_escape get\rusname%></>
+  <><%xml_escape get\class0%></>
+  <1><%xml_escape get\class1%></1>
+  <2><%xml_escape get\class2%></2>
+  <3><%xml_escape get\class3%></3>
+  <_ rows="3"><%xml_cdata get\descript%></_>
+  <_ rows="3"><%xml_cdata get\geobotdescr%></_>
+  <_><%xml_escape get\contributors%></_>
+</_>
+----------------------------------------------------------------------
+
+ SQL-,    
+   rangcode, rusname, class0, class1, class2,
+class3, descript, geobotdescr  contributors ( biotope.sql):
+
+----------------------------------------------------------------------
+SELECT t1.*,
+
+(SELECT t2.rusname FROM biotopelist AS t2
+WHERE Left(t2.rangcode, 1) = Left(t1.rangcode, 1)
+AND Right(t2.rangcode, 8)  = '00000000'
+AND Right(t1.rangcode, 8) <> '00000000') AS class0,
+
+(SELECT t2.rusname FROM biotopelist AS t2
+WHERE Left(t2.rangcode, 3) = Left(t1.rangcode, 3)
+AND Right(t2.rangcode, 6)  = '000000'
+AND Right(t1.rangcode, 6) <> '000000') AS class1,
+
+(SELECT t2.rusname FROM biotopelist AS t2
+WHERE Left(t2.rangcode, 5) = Left(t1.rangcode, 5)
+AND Right(t2.rangcode, 4)  = '0000'
+AND Right(t1.rangcode, 4) <> '0000') AS class2,
+
+(SELECT t2.rusname FROM biotopelist AS t2
+WHERE Left(t2.rangcode, 7) = Left(t1.rangcode, 7)
+AND Right(t2.rangcode, 2)  = '00'
+AND Right(t1.rangcode, 2) <> '00') AS class3
+
+FROM biotopelist AS t1
+WHERE rusname
+----------------------------------------------------------------------
+
+ biotope.template   <%include\photo.template%>,
+    ( photo.template):
+
+----------------------------------------------------------------------
+    < num="<%xml_escape get\NUMBER%>"
+       <%optional:="<%xml_escape get\authphoto%>" %>text="" 
+       file="<%xml_escape image:biotopephotos/<%get\photo%> images/<%get\SUPER.code%>_<%get\NUMBER%>.jpg jpg 300 300%>" 
+        big="<%xml_escape copy:biotopephotos/<%get\photo%> images/big/<%get\SUPER.code%>_<%get\NUMBER%>.jpg%>"/>
+----------------------------------------------------------------------
+
+ SQL-    
+(photo.sql):
+
+----------------------------------------------------------------------
+SELECT * from photos WHERE biotope = ?
+----------------------------------------------------------------------
+
+,    ( classes.template):
+
+----------------------------------------------------------------------
+<?xml version="1.0" encoding="ISO-8859-5"?>
+<><%query:class_sql [%\
+  <!-- rangcode: <%xml_escape get\rangcode%> -->
+  < id="<%get\NUMBER%>"><%xml_escape get\rusname%></>%]%>
+</>
+----------------------------------------------------------------------
+
+ SQL- (class.sql):
+
+----------------------------------------------------------------------
+SELECT t1.*
+FROM biotopelist AS t1
+WHERE rusname AND Right(t1.rangcode, 8)  = '00000000'
+  AND EXISTS
+    (SELECT NULL FROM biotopelist AS t2
+     WHERE Left(t2.rangcode, 1) = Left(t1.rangcode, 1)
+     AND Right(t2.rangcode, 8) <> '00000000')
+----------------------------------------------------------------------
+
+,    ( plants.template):
+
+----------------------------------------------------------------------
+<?xml version="1.0" encoding="ISO-8859-5"?>
+<><%query:plant_sql [%\
+  <!-- rangcode: <%xml_escape get\rangcode%> -->
+  <><%xml_escape get\rusname%></>%]%>
+</>
+----------------------------------------------------------------------
+
+ SQL- (plant.sql):
+
+----------------------------------------------------------------------
+SELECT t1.*
+FROM biotopelist AS t1
+WHERE rusname AND Right(t1.rangcode, 2) <> '00'
+----------------------------------------------------------------------
+
+ DbReader      index.template,
+     SQL-  
+:
+
+----------------------------------------------------------------------
+<%!:
+  <%prepare:biotope_sql <%read\biotope.sql%>%>
+  <%prepare:photo_sql <%read\photo.sql%>%>
+  <%prepare:class_sql <%read\class.sql%>%>
+  <%prepare:plant_sql <%read\plant.sql%>%>
+
+  <%query:biotope_sql [%\<%write:<%get\code%>.xml <%include\biotope.template%>%> %]%>
+
+  <%write:classes.xml <%include\classes.template%>%>
+  <%write:plants.xml <%include\plants.template%>%>
+%>
+----------------------------------------------------------------------
+
+  XML-:
+
+----------------------------------------------------------------------
+<?xml version="1.0" encoding="ISO-8859-5"?>
+<!DOCTYPE _ SYSTEM "brief.dtd">
+<!-- rangcode: E04020302 -->
+<_ show="0">
+  < display="0">    
+    < num="1" =" .." text="" 
+                file="images/3_1.jpg" 
+                big="images/big/3_1.jpg"/>
+    < num="2" =" .." text="" 
+                file="images/3_2.jpg" 
+                big="images/big/3_2.jpg"/>
+  </>
+  <>Betula sp. - Avenella flexuosa - Polytrichum commune</>
+  <>E.  </>
+  <1> </1>
+  <2></2>
+  <3></3>
+  <_ rows="3"><![CDATA[       .   5 - 7            ,   ,   ,   .]]></_>
+  <_ rows="3"></_>
+  <_></_>
+</_>
+----------------------------------------------------------------------
+
+
+*   DbReader
+
+ DbReader     Java  
+   :
+
+ru.karrc.dbreader.DbReader: c   . 
+,       
+ JDBC,    .
+
+ru.karrc.dbreader.TemplateParser:   ,
+    .
+
+ru.karrc.dbreader.Function:  .   
+DbReader   .
+
+ru.karrc.dbreader.FunctionDataParser:  
+.      , 
+.       
+ TemplateParser.
+
+ru.karrc.dbreader.Functions:     
+ ,  Function.
+
+ru.karrc.dbreader.TemplateParser: ,   
+    .
+
+ ,    , 
+    .
+
+
+*    .
+
+   : 8,
+ Java- ( ): 27,
+    : 1311,
+   (    ): 812,
+  : 180.
+
+
+* 
+
+     DbReader, 
+      
+      .   
+DbReader   "  " 
+  XML-    .
diff --git a/sample/class.xml b/sample/class.xml
new file mode 100644
--- /dev/null
+++ b/sample/class.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="ISO-8859-5"?>
+<>
+  < id="1"> </>
+  < id="2">   </>
+  < id="3">   </>
+  < id="4">,    </>
+  < id="5">   </>
+  < id="6"> </>
+</>
diff --git a/sample/plants.xml b/sample/plants.xml
new file mode 100644
--- /dev/null
+++ b/sample/plants.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="ISO-8859-5"?>
+<>
+  <>Betula sp.</>
+  <>Calamagrostis arundinacea</>
+  <>Geranium sylvaticum</>
+</>
diff --git a/src/Manifest.mf b/src/Manifest.mf
new file mode 100644
--- /dev/null
+++ b/src/Manifest.mf
@@ -0,0 +1,2 @@
+Manifest-Version: 1.0
+Main-Class: kryshen.tema.Tema
diff --git a/src/kryshen/tema/Function.java b/src/kryshen/tema/Function.java
new file mode 100644
--- /dev/null
+++ b/src/kryshen/tema/Function.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2005, 2006 Mikhail A. Kryshen
+ *
+ * $Id: Function.java,v 1.1.1.1 2006/05/16 14:04:09 mikhail Exp $
+ */
+
+package kryshen.tema;
+
+import java.io.*;
+import java.util.*;
+import java.sql.ResultSet;
+
+public abstract class Function {
+    public final String name;
+
+    protected Function(String name) {
+	this.name = name;
+    }
+
+    public abstract int invoke(FunctionDataParser fdp, Writer out)
+        throws IOException, TemplateException;
+}
diff --git a/src/kryshen/tema/FunctionDataParser.java b/src/kryshen/tema/FunctionDataParser.java
new file mode 100644
--- /dev/null
+++ b/src/kryshen/tema/FunctionDataParser.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2006 Mikhail A. Kryshen
+ *
+ * $Id: FunctionDataParser.java,v 1.1.1.1 2006/05/16 14:04:09 mikhail Exp $
+ */
+
+package kryshen.tema;
+
+import java.io.*;
+import java.util.*;
+
+import static kryshen.tema.TemplateParser.Result;
+import static kryshen.tema.TemplateParser.FunctionData;
+import static kryshen.tema.TemplateParser.Terminator;
+
+/**
+ * Parser for a function data.
+ *
+ * @author Mikhail A. Kryshen
+ */
+public class FunctionDataParser {
+    static final char[] ARG_SEPARATORS = TemplateParser.LIST_SEPARATORS;
+
+    private TemplateParser tp;
+    private FunctionData fd;
+    private Reader in;
+
+    private boolean available = true;
+    private boolean first = true;
+
+    FunctionDataParser(TemplateParser tp, FunctionData fd,
+                       Reader in) {
+        this.tp = tp;
+        this.fd = fd;
+        this.in = in;
+    }
+
+    private TemplateParser.Result parseData(Writer out, boolean argument)
+	throws IOException, TemplateException {
+        
+        if (!available)
+            throw new TemplateException
+		("Unexpected end of instruction data.");
+
+        TemplateParser.Result r;
+
+        if (fd == FunctionData.RECURSIVE) {
+            r = tp.parse(in, out, true, 
+                         argument ? ARG_SEPARATORS : null,
+                         (argument || !first) ? ARG_SEPARATORS : null);
+        } else if (fd == FunctionData.NONRECURSIVE) {
+            r = tp.parse(in, out, false, 
+                         argument ? ARG_SEPARATORS : null,
+                         (argument || !first) ? ARG_SEPARATORS : null);
+        } else if (fd == FunctionData.SUBFUNCTION && argument) {
+            /* Subfunction can pass a list of arguments */
+            StringWriter sw = new StringWriter();
+            tp.parseFunction(in, sw);
+            sw.close();
+            in = new StringReader(sw.toString());
+            fd = FunctionData.NONRECURSIVE;
+            r = parseData(out, true);
+        } else if (fd == FunctionData.SUBFUNCTION) {
+            r = new Result(null, tp.parseFunction(in, out), false);
+        } else {
+            throw new IllegalArgumentException("Invalid FunctiodData: " + fd);
+        }
+
+        if (r.terminator != TemplateParser.Terminator.SEPARATOR)
+            available = false; // No more function data available.
+
+        first = false;
+        return r;
+    }
+
+    public int parseData(Writer out) throws IOException, TemplateException {
+        return parseData(out, false).substitutions; 
+    }
+
+    public String getData() throws IOException, TemplateException {        
+        StringWriter sw = new StringWriter();
+        parseData(sw);
+        sw.close();
+        return sw.toString();
+    }
+
+    public int parseNextArg(Writer out) throws IOException, TemplateException {
+        TemplateParser.Result r = parseData(out, true);
+
+        // Ignore empty arguments.
+        while (r.empty && available) {
+            r = parseData(out, true);
+        }
+
+        return r.substitutions;
+    }
+
+    public String getNextArg() throws IOException, TemplateException {        
+        StringWriter sw = new StringWriter();
+        parseNextArg(sw);
+        sw.close();
+        return sw.toString();
+    }
+
+    public List<String> getArgs() throws IOException, TemplateException {
+	List<String> l = new ArrayList<String>();
+
+	while (available) {
+	    l.add(getNextArg());
+	}
+	
+	return l;
+    }
+
+    public boolean hasMoreData() {
+        return available;
+    }
+
+    public TemplateParser getTemplateParser() {
+        return tp;
+    }
+}
diff --git a/src/kryshen/tema/Functions.java b/src/kryshen/tema/Functions.java
new file mode 100644
--- /dev/null
+++ b/src/kryshen/tema/Functions.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2005, 2006 Mikhail A. Kryshen
+ *
+ * $Id: Functions.java,v 1.1.1.1 2006/05/16 14:04:09 mikhail Exp $
+ */
+
+package kryshen.tema;
+
+import java.io.*;
+import java.util.*;
+
+import java.sql.SQLException;
+import java.sql.PreparedStatement;
+
+import kryshen.tema.functions.*;
+
+public class Functions {
+    public static final Function GET =
+        new Function("get") {
+            public int invoke(FunctionDataParser fdp, Writer out)
+                throws IOException, TemplateException {
+
+		String name = fdp.getData();
+                return fdp.getTemplateParser().parseVariable(name, out);
+            }
+        };
+
+    public static final Function SET =
+        new Function("set") {
+            public int invoke(FunctionDataParser fdp, Writer out)
+                throws IOException, TemplateException {
+
+                String arg0 = fdp.getNextArg();
+                String arg1 = fdp.getData();
+                
+                fdp.getTemplateParser().setValue(arg0, arg1);
+                
+		out.write(arg0);
+                return 1;
+            }
+        };
+
+    /* prepare:qury_name sql_statement */
+    public static final Function PREPARE =
+        new Function("prepare") {
+            public int invoke(FunctionDataParser fdp, Writer out)
+                throws IOException, TemplateException {
+
+                String arg0 = fdp.getNextArg();
+                String data = fdp.getData();
+
+		PreparedStatement statement;
+
+		try {
+		    statement = Tema.connection.prepareStatement(data);
+		} catch (SQLException e) {
+		    throw new TemplateException(e.getMessage(), e);
+		}
+                
+                fdp.getTemplateParser().setValue(arg0, statement);   
+             
+		out.write(arg0);
+                return 1;
+            }
+        };
+
+    public static final Function OPTIONAL =
+        new Function("optional") {
+            public int invoke(FunctionDataParser fdp, Writer out)
+                throws IOException, TemplateException {
+
+                StringWriter sw = new StringWriter();
+                int s = fdp.parseData(sw);
+                sw.close();
+                String data = sw.toString();
+
+                if (s > 0) {
+                    out.write(data);
+                    return 1;
+                } else {
+                    return 0;
+                }
+            }
+        };
+
+    public static final Function IMAGE =
+        new Function("image") {
+            public int invoke(FunctionDataParser fdp, Writer out)
+                throws IOException, TemplateException {
+
+                String arg0 = fdp.getNextArg();
+                String arg1 = fdp.getNextArg();
+                String arg2 = fdp.getNextArg();
+                int arg3 = fdp.hasMoreData() ? 
+		    Integer.parseInt(fdp.getNextArg()) : -1;
+                int arg4 = fdp.hasMoreData() ? 
+		    Integer.parseInt(fdp.getNextArg()) : -1;
+
+                File source = new File
+                    (Tema.getProperty("resource_base"), arg0);
+
+                try {
+                    ImageConverter.convert
+                        (source,                  /* source file */
+                         new File(arg1),          /* dest file */
+                         arg2,                    /* format */
+                         arg3,                    /* max width */
+                         arg4);                   /* max height */
+                } catch (Exception e) {
+                    System.err.println(e);
+                    return 0;
+                }
+
+                out.write(arg1);
+                return 1;
+            }
+        };
+
+    public static final Function COPY =
+        new Function("copy") {
+           public int invoke(FunctionDataParser fdp, Writer out)
+                throws IOException, TemplateException {
+
+                String arg0 = fdp.getNextArg();
+                String arg1 = fdp.getNextArg();
+
+                File source = new File
+                    (Tema.getProperty("resource_base"), arg0);
+                File dest = new File(arg1);
+                
+                try {
+                    copyFile(source, dest);
+                } catch (IOException e) {
+                    System.err.println(e);
+                    return 0;
+                }
+                out.write(arg1);
+                return 1;
+            }
+        };
+
+    /* query:sql_query template arg1 arg2 ... argN */
+    public static final Function QUERY =
+        new Function("query") {
+           public int invoke(FunctionDataParser fdp, Writer out)
+                throws IOException, TemplateException {
+                
+                String arg0 = fdp.getNextArg();
+                String arg1 = fdp.getNextArg();
+		List<String> args = fdp.getArgs();				
+
+		TemplateParser tp = fdp.getTemplateParser();
+
+		try {
+		    return Tema.query
+			(arg1, (PreparedStatement)tp.getValue(arg0),
+			 args, tp, out);
+		} catch (SQLException e) {
+		    throw new TemplateException(e.getMessage(), e);
+		}
+            }
+        };
+
+    public static final Function WRITE =
+        new Function("write") {
+           public int invoke(FunctionDataParser fdp, Writer out)
+                throws IOException, TemplateException {
+
+                String arg0 = fdp.getNextArg();
+
+		System.err.println("Writing " + arg0 + "...");
+
+                Writer fw;
+
+                try {
+		    fw = Tema.createFileWriter(arg0);
+                } catch (IOException e) {
+                    System.err.println(e);
+                    return 0;
+                }
+                
+                try {
+		    fdp.parseData(fw);
+                } finally {
+                    fw.close();
+                }
+
+                out.write(arg0);
+		
+		System.err.println("Saved " + arg0 + ".");
+                return 1;
+            }
+        };
+
+    public static final Function READ =
+        new Function("read") {
+           public int invoke(FunctionDataParser fdp, Writer out)
+                throws IOException, TemplateException {
+
+	       String filename = fdp.getData();
+	       String file;
+	       
+                try {
+                    file = Tema.readFile(new File(filename));
+                } catch (IOException e) {
+                    System.err.println(e);
+                    return 0;
+                }
+		
+                out.write(file);
+                return 1;
+            }
+        };
+
+    public static final Function INCLUDE =
+        new Function("include") {
+           public int invoke(FunctionDataParser fdp, Writer out)
+                throws IOException, TemplateException {
+                
+                String filename = fdp.getData();
+                
+                Reader fin = new BufferedReader
+		    (Tema.createCachedFileReader(new File(filename)));
+                
+                int r = fdp.getTemplateParser().parse(fin, out);
+
+                fin.close();
+                return r;
+            }
+        };
+
+    public static final Function NULL_OUTPUT =
+        new Function("!") {
+	    public int invoke(FunctionDataParser fdp, Writer out)
+		throws IOException, TemplateException {		
+
+		/* Write nothing. */
+		return fdp.parseData(new Writer() {
+                    public void close() {}
+                    public void flush() {}
+                    public void write(char[] cbuf, int off, int len) {}
+                });
+            }
+        };
+
+    public static final Function REPLACE =
+        new Function("replace") {
+	    public int invoke(FunctionDataParser fdp, Writer out)
+		throws IOException, TemplateException {		
+
+                String arg0 = fdp.getNextArg();
+                String arg1 = fdp.getNextArg();
+
+                ReplaceWriter rw = new ReplaceWriter(out, arg0, arg1);
+
+                int ret = fdp.parseData(rw);
+                rw.finish();
+                return ret;
+            }
+        };
+
+    public static final Function XML_ESCAPE =
+        new Function("xml_escape") {
+	    public int invoke(FunctionDataParser fdp, Writer out)
+		throws IOException, TemplateException {		
+
+                final String[] chars = {"&", "<", ">", "`", "\""};
+                final String[] escape = {"&", "<", ">", "'", """};
+
+                ReplaceWriter rw = new ReplaceWriter(out, chars, escape);
+
+                int ret = fdp.parseData(rw);
+                rw.finish();
+                return ret;
+            }
+        };
+
+    public static final Function XML_CDATA =
+        new Function("xml_cdata") {
+	    public int invoke(FunctionDataParser fdp, Writer out)
+		throws IOException, TemplateException {		
+
+                ReplaceWriter rw = new ReplaceWriter
+                    (out, "]]>", "]]]><![CDATA[]>", "<![CDATA[", "]]>");
+
+                int ret = fdp.parseData(rw);
+                rw.finish();
+                return ret;
+            }
+        };
+    
+    public static final Function DEFINE = new Define();
+
+    private static void copyFile(File src, File dest) throws IOException {
+	System.err.print("Copying " + src + "... ");
+	
+	if (src.lastModified() < dest.lastModified()) {
+	    System.err.println(dest + " is up to date.");
+	    return;
+	}
+
+	File parent = dest.getParentFile();
+	if (parent != null) parent.mkdirs();
+
+        InputStream in = new FileInputStream(src);
+        OutputStream out = new FileOutputStream(dest);
+        
+        byte[] buffer = new byte[1024];
+        int len;
+        while ((len = in.read(buffer)) > 0) {
+            out.write(buffer, 0, len);
+        }
+        in.close();
+        out.close();
+
+	System.err.println("saved " + dest + ".");
+    }
+
+    static void registerAllFunctions(TemplateParser p) {
+        p.registerFunction(GET);
+        p.registerFunction(SET);
+        p.registerFunction(OPTIONAL);
+        p.registerFunction(IMAGE);
+        p.registerFunction(COPY);
+        p.registerFunction(QUERY);
+        p.registerFunction(PREPARE);
+        p.registerFunction(WRITE);
+        p.registerFunction(READ);
+        p.registerFunction(INCLUDE);
+        p.registerFunction(NULL_OUTPUT);
+        p.registerFunction(REPLACE);
+        p.registerFunction(XML_ESCAPE);
+        p.registerFunction(XML_CDATA);
+        p.registerFunction(DEFINE);
+    }
+}
diff --git a/src/kryshen/tema/ImageConverter.java b/src/kryshen/tema/ImageConverter.java
new file mode 100644
--- /dev/null
+++ b/src/kryshen/tema/ImageConverter.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2005, 2006 Mikhail A. Kryshen
+ *
+ * $Id: ImageConverter.java,v 1.1.1.1 2006/05/16 14:04:09 mikhail Exp $
+ */
+
+package kryshen.tema;
+
+import java.util.*;
+import java.io.*;
+import java.awt.*;
+import java.awt.image.*;
+import java.awt.geom.AffineTransform;
+import javax.imageio.*;
+import javax.imageio.stream.*;
+
+/**
+ * Class defines static method to convert images to specified format.
+ *
+ * @author Mikhail A. Kryshen
+ */
+class ImageConverter {    
+
+    static void convert(File source, File dest, String format,
+		 int maxWidth, int maxHeight) 
+	throws IOException, InterruptedException {
+	
+	System.err.print("Converting image " + source + "... ");
+
+	if (source.lastModified() < dest.lastModified()) {
+	    System.err.println(dest + " is up to date.");
+	    return;
+	}
+
+	BufferedImage image = ImageIO.read(source);
+
+	//int type = image.getType();
+	//final int type = BufferedImage.TYPE_INT_RGB;
+	//ColorModel cm = image.getColorModel();
+
+        int w = image.getWidth(null);
+        int h = image.getHeight(null);
+
+	//boolean scale = false;
+
+	float scale = 1f;
+
+	if (maxWidth > 0 && w > maxWidth)
+	    scale = (float)maxWidth / w;
+	
+	if (maxHeight > 0 && h * scale > maxHeight)
+	    scale = (float)maxHeight / h;
+
+	if (scale != 1f) {
+	    w *= scale; h *= scale;
+
+// 	    ColorModel cm = image.getColorModel();	    
+// 	    boolean alphaPremultiplied = cm.isAlphaPremultiplied();
+// 	    WritableRaster raster = cm.createCompatibleWritableRaster(w, h);
+// 	    BufferedImage scaled = new BufferedImage
+// 		(cm, raster, alphaPremultiplied, null);
+
+// 	    BufferedImage scaled = new BufferedImage(w, h, image.getType());
+	    
+
+ 	    Image scaled = image.getScaledInstance(w, h, Image.SCALE_SMOOTH);
+
+ 	    ColorModel cm = image.getColorModel();	    
+ 	    boolean alphaPremultiplied = image.isAlphaPremultiplied();
+ 	    WritableRaster raster = cm.createCompatibleWritableRaster(w, h);
+ 	    image = new BufferedImage(cm, raster, alphaPremultiplied, null);
+
+	    Graphics g = image.getGraphics();
+	    g.drawImage(scaled, 0, 0, null);
+	    g.dispose();
+
+//	    image = scale(image, scaled, scale);
+//	    Graphics2D g = scaled.createGraphics();
+//	    g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
+// 			       RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+// 	    System.err.print(" " + g.drawImage(image, 0, 0, w, h, null) + " ");
+// 	    g.dispose();
+// 	    image = scaled;
+	}
+       
+	File parent = dest.getParentFile();
+	if (parent != null) parent.mkdirs();
+
+ 	ImageIO.write(image, format, dest);
+	System.err.println("saved " + dest + ".");	
+    }
+
+//     public static ColorModel getColorModel(Image image) 
+// 	throws InterruptedException {
+
+// 	PixelGrabber grabby = new PixelGrabber(image, 0, 0, 1, 1, false);
+// 	if (!grabby.grabPixels())
+// 	    throw new RuntimeException("pixel grab fails");
+// 	return grabby.getColorModel();
+//     }
+
+//     private static BufferedImage scale(BufferedImage image, 
+// 				       BufferedImage dest, 
+// 				       float scale) {
+
+// 	AffineTransform tx = new AffineTransform();
+// 	tx.scale(scale, scale);
+
+//  	AffineTransformOp op = new AffineTransformOp
+//  	    (tx, AffineTransformOp.TYPE_BILINEAR);
+
+// 	return op.filter(image, dest);
+//     }
+
+    // TEST
+    
+    public static void main(String[] args) 
+	throws IOException, InterruptedException {
+
+	convert(new File(args[0]), new File(args[1]), "png", 300, 300);
+    }
+
+
+//     public static BufferedImage toBufferedImage(Image image, ColorModel cm) {
+// 	if (image instanceof BufferedImage)
+// 	    return (BufferedImage)image;
+
+// 	int w = image.getWidth(null);
+// 	int h = image.getHeight(null);
+
+// 	boolean alphaPremultiplied = cm.isAlphaPremultiplied();
+// 	WritableRaster raster = cm.createCompatibleWritableRaster(w, h);
+// 	BufferedImage result = new BufferedImage(cm, raster, alphaPremultiplied, null);
+// 	Graphics2D g = result.createGraphics();
+
+// 	g.drawImage(image, 0, 0, null);
+// 	g.dispose();
+
+// 	return result;
+//     }
+}
+
+
+// This is how I'd recommend it in JDK1.4 using javax.imageio.ImageIO to
+// do the reading/writing and Java2D to do the scaling.
+
+// // read in the original image
+// BufferedImage in = ImageIO.read(new File("in.jpg"));
+
+// // create a new image of the smaller size
+// BufferedImage out = new BufferedImage(newWidth, newHeight);
+
+// // get the graphics associated with the new image
+// Graphics g = out.getGraphics();
+
+// // draw the original image into the new image, scaling
+// // on the fly
+// g.drawImage(in, newWidth, newHeight, null);
+
+// // dispose the graphics object
+// g.dispose();
+
+// // write out the new image
+// ImagIO.write("out.jpg", "jpeg", new File("out.jpg"));
+
+// ------------------------------------------
+
+// For nicer scaling, you could add a rendering hint to the graphics
+// object before painting.
+
+// ie.
+
+// Graphics2D g2d = (Graphics2D)g;
+// g2d.setRenderingHint(RenderingHints.KEY_INTEPOLATION,
+//                      RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+
+// or use VALUE_INTERPOLATION_BICUBIC for the best.
+
+// But as you can guess, nicer == slower.
+
+// Shannon Hickey
+// shann...@swingteam.com
+// Swing Team  http://TheSwingConnection.com  http://TheJavaTutorial.com
+// Java Software,  a division of Sun Microsystems, Inc. 
diff --git a/src/kryshen/tema/ReplaceWriter.java b/src/kryshen/tema/ReplaceWriter.java
new file mode 100644
--- /dev/null
+++ b/src/kryshen/tema/ReplaceWriter.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2006 Mikhail A. Kryshen
+ *
+ * $Id: ReplaceWriter.java,v 1.1.1.1 2006/05/16 14:04:09 mikhail Exp $
+ */
+
+package kryshen.tema;
+
+import java.io.Writer;
+import java.io.FilterWriter;
+import java.io.IOException;
+
+import java.util.LinkedList;
+import java.util.Iterator;
+
+/**
+ * FilterWriter which replaces characters with escape strings.
+ *
+ * @author Mikhail A. Kryshen
+ */
+class ReplaceWriter extends FilterWriter {
+    private String[] patterns;
+    private String[] replaces;
+
+    private String prefix;
+    private String postfix;
+
+    private int maxPatternLength = 0;
+
+    /** Had this Writer processed any data? */
+    private boolean processedData = false;
+
+    private StringBuffer buffer = new StringBuffer();
+
+    public ReplaceWriter(Writer w, String pattern, String replace) {
+        this(w, new String[]{pattern}, new String[]{replace});
+    }
+
+    public ReplaceWriter(Writer w, String pattern, String replace,
+                         String prefix, String postfix) {
+
+        this(w, new String[]{pattern}, new String[]{replace}, 
+             prefix, postfix);
+    }
+
+    public ReplaceWriter(Writer w, String[] patterns, String[] replaces) {
+        this(w, patterns, replaces, null, null);
+    }
+
+    public ReplaceWriter(Writer w, String[] patterns, String[] replaces, 
+                         String prefix, String postfix) {
+	super(w);
+        
+	this.patterns = patterns;
+	this.replaces = replaces;
+        
+        this.prefix = prefix;
+        this.postfix = postfix;
+        
+	for (int i = 0; i < patterns.length; i++) {
+	    int length = patterns[i].length();
+	    if (length > maxPatternLength)
+		maxPatternLength = length;
+	}
+    }
+
+    private boolean processBuffer(int minLength) throws IOException {
+        if (buffer.length() == 0) return false;
+
+        if (!processedData) {
+            if (prefix != null)
+                super.write(prefix, 0, prefix.length());
+            processedData = true;
+        }
+
+        int k;
+	for (k = 0; minLength + k < buffer.length(); k++) {
+	    for (int i = 0; i < patterns.length; i++) {
+		String pattern = patterns[i];
+		int length = pattern.length();
+
+                if (length + k > buffer.length())
+                    continue;
+
+		boolean match = true;
+
+		for (int j = 0; j < length; j++) {
+		    if (pattern.charAt(j) != buffer.charAt(j + k)) {
+			match = false;
+                        break;
+		    }
+		}
+
+		if (match) {
+                    super.write(buffer.substring(0, k), 0, k);
+                    buffer.delete(0, k + length);
+                    super.write(replaces[i], 0, replaces[i].length());
+		    return true;
+		}
+	    }
+	}
+
+        super.write(buffer.substring(0, k), 0, k);
+        buffer.delete(0, k);
+
+        return false;
+    }
+
+    public void write(int c) throws IOException {
+	buffer.append((char)c);
+	processBuffer(maxPatternLength);
+    }
+
+    public void write(char[] cbuf, int off, int len) throws IOException {
+	for (int i = off; i < off + len; i++) {
+	    buffer.append(cbuf[i]);
+	}
+	processBuffer(maxPatternLength);
+    }
+
+    public void write(String str, int off, int len) throws IOException {
+        buffer.append(str.substring(off, off + len));
+	processBuffer(maxPatternLength);
+    }
+
+    public void finish() throws IOException {
+        while (processBuffer(0));
+
+//         super.write(buffer.toString(), 0, buffer.length());
+//         buffer.delete(0, buffer.length());
+
+        if (processedData && postfix != null)
+            super.write(postfix, 0, postfix.length());
+    }
+
+    public void close() throws IOException {
+        finish();
+        super.close();
+    }
+}
diff --git a/src/kryshen/tema/Tema.java b/src/kryshen/tema/Tema.java
new file mode 100644
--- /dev/null
+++ b/src/kryshen/tema/Tema.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2005, 2006 Mikhail A. Kryshen
+ *
+ * $Id: Tema.java,v 1.1.1.1 2006/05/16 14:04:09 mikhail Exp $
+ */
+
+package kryshen.tema;
+
+import java.sql.Driver;
+import java.sql.DriverManager;
+import java.sql.Connection;
+import java.sql.Statement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Properties;
+import java.util.List;
+import java.util.Collections;
+
+import java.io.*;
+
+/**
+ * Tema main class.
+ *
+ * @author Mikhail A. Kryshen
+ */
+public class Tema {
+    private static final String CONFIG_FILE = "tema.properties";
+    private static final String ENV_PREFIX = "kryshen.tema.";
+
+    private static Properties config = new Properties();
+    static Connection connection = null;
+
+    private static String inputEncoding;
+    private static String outputEncoding;
+
+    static Map<File, String> fileCache = null;
+
+    public static void main(String[] args) 
+	throws IOException, TemplateException, SQLException,
+	       ClassNotFoundException, InstantiationException, 
+	       IllegalAccessException {
+	
+        InputStream configIn = new FileInputStream(CONFIG_FILE);
+        config.load(configIn);
+        configIn.close();
+
+        String logFile = getProperty("log");
+        if (logFile != null && logFile.length() > 0)
+            System.setErr(new PrintStream(new FileOutputStream(logFile)));
+
+	if (Boolean.parseBoolean(getProperty("cache_read")) == true)
+	    fileCache = new HashMap<File, String>();
+
+	inputEncoding = getProperty("input_encoding", "iso-8859-1");
+	outputEncoding = getProperty("output_encoding", "iso-8859-1");
+
+	String resourceProperty = getProperty("resource");
+
+	if (resourceProperty != null) {
+	    String driverProperty = getProperty("driver");
+	    if (driverProperty != null) {
+		// Register driver.  
+		Driver d = (Driver)Class.forName(driverProperty).newInstance();
+	    }
+	
+	    // Open connection.
+	    connection = DriverManager.getConnection(resourceProperty);
+	}
+	
+// 	String maxFilesString = getProperty("max_output_files");
+// 	int maxFiles = -1;
+// 	if (maxFilesString != null)
+// 	    maxFiles = Integer.parseInt(maxFilesString);
+
+	String main = getProperty("main_template");
+	String output = getProperty("output");
+	
+	Writer out;
+
+	if ("stderr".equals(output))
+	    out = new OutputStreamWriter(System.err);
+	else
+	    out = new OutputStreamWriter(System.out);
+
+	InputStream is = new FileInputStream(main);
+        Reader in = new BufferedReader(new InputStreamReader(is, inputEncoding));
+
+        TemplateParser p = new TemplateParser();
+	try {
+	    p.parse(in, out);
+	} finally {
+	    in.close();
+	    out.flush();
+	}  
+
+	System.err.println("Done");
+    }
+
+//     private static void readEscapes() {
+// 	for (int i = 1;; i++) {
+// 	    String pattern = getProperty("escape_char_" + i);
+
+// 	    if (pattern == null) break;
+
+// 	    escapes.put(pattern.charAt(0),
+// 			getProperty("escape_string_" + i));
+// 	}
+//     }
+
+    /**
+     * Get configuration property.
+     */
+    static String getProperty(String name) {
+        String value = System.getProperty(ENV_PREFIX + name);
+        if (value != null) return value;
+        return config.getProperty(name);
+    }
+
+    /**
+     * Get configuration property.
+     */
+    static String getProperty(String name, String fallback) {
+	String value = getProperty(name);
+	if (value != null) return value;
+	return fallback;
+    }
+
+    /**
+     * Process subquery and template.
+     *
+     * @param template template to fill with data.
+     * @param ps query statement to execute.
+     * @param args list of query parameters.
+     * @param superParser invoking object.
+     * @param out Writer to output processed data.
+     *
+     * @return number of processed rows in query result.
+     */
+    static int query(String template, PreparedStatement ps,
+		     List<String> args,
+		     TemplateParser superParser, 
+                     Writer out)
+	throws IOException, SQLException, TemplateException {
+
+	StringReader templateReader = new StringReader(template);
+
+	int i = 1;
+
+	for (String arg : args) {
+	    ps.setString(i++, arg);
+	}
+	
+	ResultSet r = ps.executeQuery();
+	ResultSetMetaData rm = r.getMetaData();
+	int columnCount = rm.getColumnCount();
+
+	String[] names = new String[columnCount];
+	
+	for (int j = 0; j < columnCount; j++) {
+	    names[j] = rm.getColumnName(j + 1);
+	}
+
+	TemplateParser p = new TemplateParser(superParser);
+	
+	for (i = 1; r.next(); i++) {
+	    p.clearValues();
+
+	    for (int j = 0; j < columnCount; j++) {
+		Object value = r.getObject(j + 1);
+		if (r.wasNull()) value = null;
+
+		p.setValue(names[j], value);
+	    }
+
+	    p.setValue("NUMBER", i);
+            p.parse(templateReader, out);
+	    templateReader.reset();
+	}
+
+	r.close();
+	ps.clearParameters();
+	return i - 1;
+    }
+
+    static Writer createFileWriter(String filename) throws IOException {
+	OutputStream os = new FileOutputStream(filename);
+	return new OutputStreamWriter(os, Tema.outputEncoding);
+    }
+
+    static Reader createFileReader(File file) throws IOException {
+	InputStream is = new FileInputStream(file);
+	return new InputStreamReader(is, Tema.inputEncoding);
+    }
+
+    static Reader createCachedFileReader(File file) throws IOException {
+	if (fileCache != null) return new StringReader(readFile(file));
+	else return createFileReader(file);
+    }
+
+    /**
+     * Read text file.
+     */
+    static String readFile(File file) throws IOException {
+	String data;
+
+	if (fileCache != null) {
+	    data = fileCache.get(file);	    
+	    if (data != null) return data;
+	}
+
+        Reader r = new BufferedReader(createFileReader(file));
+
+        // FIXME: possible overflow (long -> int).
+	StringBuffer sb = new StringBuffer((int)file.length());
+
+	try {
+ 	    for (int c = r.read(); c >= 0; c = r.read()) {
+ 		sb.append((char)c);
+ 	    }
+	} finally {
+	    r.close();
+	}
+
+	data = sb.toString();
+
+	if (fileCache != null) {
+	    fileCache.put(file, data);
+	}
+
+	return data;
+    }
+}
diff --git a/src/kryshen/tema/TemplateException.java b/src/kryshen/tema/TemplateException.java
new file mode 100644
--- /dev/null
+++ b/src/kryshen/tema/TemplateException.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2006 Mikhail A. Kryshen
+ *
+ * $Id: TemplateException.java,v 1.1.1.1 2006/05/16 14:04:09 mikhail Exp $
+ */
+
+package kryshen.tema;
+
+/**
+ * Signals that template parsing fails.
+ *
+ * @author Mikhail A. Kryshen
+ */
+public class TemplateException extends Exception {
+    // TODO: store the number of template line with error.
+
+    public TemplateException(String message) {
+	super(message);
+    }
+
+    public TemplateException(String message, Throwable cause) {
+	super(message, cause);
+    }
+}
diff --git a/src/kryshen/tema/TemplateParser.java b/src/kryshen/tema/TemplateParser.java
new file mode 100644
--- /dev/null
+++ b/src/kryshen/tema/TemplateParser.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2005, 2006 Mikhail A. Kryshen
+ *
+ * $Id: TemplateParser.java,v 1.1.1.1 2006/05/16 14:04:09 mikhail Exp $
+ */
+
+package kryshen.tema;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * Parser for DbReader templates.
+ *
+ * @author Mikhail A. Kryshen
+ */
+public class TemplateParser {
+    static final String SUPER = "SUPER.";
+
+    /* Brackets. */
+    static final char[] BR_LEFT = {'<', '['};
+    static final char[] BR_RIGHT = {'>', ']'};
+
+    static final char BR_IN = '%';
+    
+    /* Separators. */
+    static final char[] REC_DATA_SEPARATORS = {':'};
+    static final char[] NONREC_DATA_SEPARATORS = {'\\', '`'}; 
+    static final char[] LIST_SEPARATORS = {' ', '\t', '\r', '\n'};
+
+    static enum FunctionData {
+	SUBFUNCTION, RECURSIVE, NONRECURSIVE;
+    };
+
+    static enum Terminator {
+	EOF, BRACKET, SEPARATOR;
+    };
+
+    static class Result {
+	Terminator terminator;
+	int substitutions;
+
+        /** No text had been parsed. */
+        boolean empty; 
+
+	Result(Terminator terminator, int substitutions, boolean empty) {
+            this.terminator = terminator;
+            this.substitutions = substitutions;
+            this.empty = empty;
+        }
+
+        Result() {
+            this(null, 0, true);
+        }       
+    };    
+
+    private Map<String, Function> functions = new HashMap<String, Function>();
+
+    private Map<String, Object> variables = new HashMap<String, Object>();
+    private TemplateParser superParser;
+
+    private int termBracket = -1;
+
+    public TemplateParser() {
+        this(null);
+    }
+
+    public TemplateParser(TemplateParser superParser) {
+	this.superParser = superParser;
+
+        Functions.registerAllFunctions(this);
+    }
+
+    public void registerFunction(Function f) {
+        functions.put(f.name, f);
+    }
+
+    public int parse(Reader in, Writer out) 
+	throws IOException, TemplateException {
+	
+	return parse(in, out, true, null, null).substitutions;
+    }
+
+    /**
+     *  Parse template.
+     *
+     * @param in Reader to read template data.
+     * @param out Writer to write processed data.
+     * @param recursive enables recursive processing.
+     * @param separators characters which terminate parsing block.
+     * @param leading characters to ignore at the beginning of the block.
+     */
+    Result parse(Reader in, Writer out, boolean recursive, 
+                 char[] separators, char[] leading)
+	throws IOException, TemplateException {
+
+	Result result = new Result();
+	int lc = -1;
+
+	while (true) {
+	    int c = in.read();
+            
+            if (leading != null && isSeparator(c, leading))
+                continue;
+            else
+                leading = null;
+
+            boolean leftBracket = false;
+            int index;
+
+            for (index = 0; index < BR_LEFT.length; index++) {
+                if (lc == BR_LEFT[index]) {
+                    leftBracket = true;
+                    break;
+                }
+            }
+
+	    if (recursive && leftBracket && c == BR_IN) {
+		
+		lc = -1;
+                int tb = termBracket;
+                termBracket = BR_RIGHT[index];
+		result.substitutions += parseFunction(in, out);
+                termBracket = tb;
+		
+	    } else if (lc == BR_IN && c == termBracket) {
+		
+		lc = -1;
+		result.terminator = Terminator.BRACKET;
+		break;
+		
+	    } else {
+
+		if (lc >= 0)
+		    out.write(lc);
+	    
+		lc = c;
+	    }
+
+	    if (c < 0) {
+		result.terminator = Terminator.EOF;
+		break; 
+	    } else if (separators != null && isSeparator(c, separators)) {
+                result.terminator = Terminator.SEPARATOR;
+                break;
+            }
+
+            result.empty = false;
+	}
+
+	return result;
+    }
+
+    int parseVariable(String name, Writer out) 
+	throws IOException, TemplateException {
+
+        Object ovalue = getValue(name);
+        if (ovalue == null)
+            return 0;
+
+	String svalue = ovalue.toString();
+
+	if (svalue == null || svalue.length() == 0)
+	    return 0;
+       
+	out.write(svalue);
+	return 1;
+    }
+
+    boolean isSeparator(int c, char[] separators) {
+        for (char p : separators) {
+            if (c == p) return true;
+        }
+
+        return false;
+    }
+
+    int parseFunction(Reader in, Writer out)
+	throws IOException, TemplateException {
+        
+	StringBuffer sb = new StringBuffer();
+        
+	while (true) {
+	    int c = in.read();
+            
+            if (isSeparator(c, LIST_SEPARATORS)) {
+		return invokeFunction(sb.toString(), 
+				      FunctionData.SUBFUNCTION,
+				      in, out);
+            } else if (isSeparator(c, REC_DATA_SEPARATORS)) {
+		return invokeFunction(sb.toString(), 
+				      FunctionData.RECURSIVE,
+				      in, out);
+            } else if (isSeparator(c, NONREC_DATA_SEPARATORS)) {
+		return invokeFunction(sb.toString(), 
+				      FunctionData.NONRECURSIVE, 
+				      in, out);
+            } else if (c < 0) {
+		System.err.println("Error: unexpected end of file.");
+		return 0;
+            } else {
+		sb.append((char)c);
+	    }
+	}
+    }
+
+    private int invokeFunction(String name, FunctionData fd,
+			       Reader in, Writer out)
+	throws IOException, TemplateException {
+
+        FunctionDataParser fdp = new FunctionDataParser(this, fd, in);
+
+	if ("".equals(name)) {
+            return fdp.parseData(out);
+	}
+        
+        Function f = getFunction(name);
+
+        if (f == null) {
+	    throw new TemplateException("Unknown function: " + name);
+        }
+
+        int r = f.invoke(fdp, out);
+
+	if (fdp.hasMoreData()) {
+	    /* Skip remaining function data. */
+	    fdp.parseData(new Writer() {
+		    public void close() {}
+		    public void flush() {}
+		    public void write(char[] cbuf, int off, int len) {}
+		});
+	}
+	
+	return r;
+    }
+
+    Function getFunction(String name) throws TemplateException {
+	Function function = functions.get(name);
+	if (function != null) return function;   
+
+	if (superParser != null) {
+	    if (name.startsWith(SUPER))
+		return superParser.getFunction(name.substring(SUPER.length()));
+	    else
+		return superParser.getFunction(name);
+	}
+
+	return null;
+    }
+
+    Object getValue(String name) throws TemplateException {
+	Object value = variables.get(name);
+	if (value != null) return value;   
+
+	if (superParser != null) {
+	    if (name.startsWith(SUPER))
+		return superParser.getValue(name.substring(SUPER.length()));
+	    else
+		return superParser.getValue(name);
+	}
+
+	return null;
+    }
+
+    void setValue(String name, Object value) {
+	variables.put(name, value);
+    }
+
+    void clearValues() {
+	variables.clear();
+    }
+
+    // TEST
+    public static void main(String[] args) 
+	throws IOException, TemplateException {
+	
+	TemplateParser p = new TemplateParser();
+	
+	FileReader in = new FileReader("parser.in");
+	FileWriter out = new FileWriter("parser.out");
+
+	p.parse(in, out);
+
+	in.close();
+	out.close();
+    }
+}
diff --git a/src/kryshen/tema/functions/Define.java b/src/kryshen/tema/functions/Define.java
new file mode 100644
--- /dev/null
+++ b/src/kryshen/tema/functions/Define.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2006 Mikhail A. Kryshen
+ *
+ * $Id: Define.java,v 1.1.1.1 2006/05/16 14:04:09 mikhail Exp $
+ */
+
+package kryshen.tema.functions;
+
+import java.io.*;
+import java.util.*;
+
+import kryshen.tema.*;
+
+public class Define extends Function {
+    public Define() {
+        super("define");
+    }
+
+    public int invoke(FunctionDataParser fdp, Writer out)
+        throws IOException, TemplateException {
+        
+        final String arg0 = fdp.getNextArg();
+        final String data = fdp.getData();
+
+        Function newFunction = new Function(arg0) {
+                public int invoke(final FunctionDataParser fdp, final Writer out)
+                    throws IOException, TemplateException {
+
+                    TemplateParser functionParser = 
+                        new TemplateParser(fdp.getTemplateParser());
+                    
+                    functionParser.registerFunction(new Function("nextarg") {
+                            public int invoke(FunctionDataParser unusedFdp, 
+                                              final Writer out)
+                                throws IOException, TemplateException {
+
+                                return fdp.parseNextArg(out);
+                            }
+                        });
+
+                    functionParser.registerFunction(new Function("data") {
+                            public int invoke(FunctionDataParser unusedFdp, 
+                                              final Writer out)
+                                throws IOException, TemplateException {
+
+                                return fdp.parseData(out);
+                            }
+                        });
+
+                    return functionParser.parse(new StringReader(data), out);                                       
+                }
+            };
+
+        fdp.getTemplateParser().registerFunction(newFunction);
+
+        out.write(arg0);
+        return 1;
+    }
+}
diff --git a/test/inctest.template b/test/inctest.template
new file mode 100644
--- /dev/null
+++ b/test/inctest.template
@@ -0,0 +1,1 @@
+<%test:1 2 3%>
diff --git a/test/macrotest b/test/macrotest
new file mode 100644
--- /dev/null
+++ b/test/macrotest
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+java -jar ../dist/tema.jar
diff --git a/test/main.template b/test/main.template
new file mode 100644
--- /dev/null
+++ b/test/main.template
@@ -0,0 +1,3 @@
+<%define`test [%nextarg:%]+[%nextarg:%]=[%data:]%>
+<%include`inctest.template%>
+<%test:<%test:ab cd x%> [%\<%test\1 2 3%>%] y%>
diff --git a/test/tema.properties b/test/tema.properties
new file mode 100644
--- /dev/null
+++ b/test/tema.properties
@@ -0,0 +1,22 @@
+# Data source configuration
+# resource          : jdbc:odbc:biotopes-data
+# driver            : sun.jdbc.odbc.JdbcOdbcDriver
+
+# Base directory for images and files
+# resource_base     : C:\\biotopes\\db
+
+# Template to start processing with
+main_template     : main.template
+
+# File encodings
+# input_encoding    : ISO-8859-5
+# output_encoding   : ISO-8859-5
+
+# Cache templates
+# cache_read        : true
+
+# Output main_template parsing result to stderr
+# output            : stderr
+
+# File to output error messages (redirect stderr)
+# log               : dbreader.log