android-ibc-forum

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

commit e27e10268ea0e04128bd4d0095bcc134702c9393
parent 6eb39d63634c0c4cdfa088b7cef1dd74ff00b57a
Author: Jan Dankert <devnull@localhost>
Date:   Sat, 28 Jan 2012 00:21:35 +0100

BB-Code-Parser, Bilder in Posts anzeigen, Layout, neue Einstellungen.

Diffstat:
res/drawable/border.xml | 8++++++++
res/layout/newsdetail.xml | 27+++++++++++++++++++++++++++
res/layout/start.xml | 6+++---
res/menu/forum_guest.xml | 6++++++
res/menu/topic_context.xml | 6++++++
res/values/strings.xml | 7+++++++
res/xml/preferences.xml | 37++++++++++++++++++++++++-------------
src/de/mtbnews/android/NewsDetailActivity.java | 45++++-----------------------------------------
src/de/mtbnews/android/adapter/ListEntryContentAdapter.java | 29++++++++++++++++++++++++-----
src/de/mtbnews/android/image/ImageGetter.java | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
src/org/mcsoxford/rss/RSSHandler.java | 2+-
src/org/mcsoxford/rss/RSSItem.java | 4++--
src/ru/perm/kefir/bbcode/AbstractCode.java | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/BBProcessor.java | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/BBProcessorFactory.java | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/Code.java | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/Configuration.java | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/ConfigurationFactory.java | 885+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/Constant.java | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/ConstantCode.java | 44++++++++++++++++++++++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/Context.java | 248+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/EscapeProcessor.java | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/EscapeXmlProcessorFactory.java | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/IntSet.java | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/NamedElement.java | 39+++++++++++++++++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/NamedValue.java | 26++++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/Pattern.java | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/PatternElement.java | 35+++++++++++++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/Scope.java | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/Source.java | 208+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/Template.java | 44++++++++++++++++++++++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/TemplateElement.java | 14++++++++++++++
src/ru/perm/kefir/bbcode/Text.java | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/TextProcessor.java | 44++++++++++++++++++++++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/TextProcessorAdapter.java | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/TextProcessorChain.java | 35+++++++++++++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/TextProcessorFactory.java | 16++++++++++++++++
src/ru/perm/kefir/bbcode/TextProcessorFactoryException.java | 24++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/Util.java | 38++++++++++++++++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/Variable.java | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/ru/perm/kefir/bbcode/default.xml | 237+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
41 files changed, 3391 insertions(+), 65 deletions(-)

diff --git a/res/drawable/border.xml b/res/drawable/border.xml @@ -0,0 +1,7 @@ +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <stroke android:width="1dp" android:color="#000000" /> + <solid android:color="#E3E3E3" /> + <padding android:left="3dp" android:top="3dp" + android:right="3dp" android:bottom="3dp" /> + <corners android:radius="2dp" /> +</shape> + \ No newline at end of file diff --git a/res/layout/newsdetail.xml b/res/layout/newsdetail.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" android:layout_width="fill_parent" + android:layout_height="wrap_content" android:background="#E3E3E3"> + <LinearLayout android:orientation="vertical" + android:layout_width="fill_parent" android:layout_height="wrap_content"> + + + <TextView android:layout_width="wrap_content" + android:layout_height="wrap_content" android:id="@+id/item_date" + style="@style/normalText"></TextView> + + <!-- + <TextView android:text="@+id/TextView03" + android:layout_width="wrap_content" + android:layout_height="wrap_content" android:id="@+id/item_title" + style="@style/titleText"></TextView> + --> + + <TextView android:text="@+id/TextView01" + android:layout_width="wrap_content" android:layout_height="wrap_content" + android:id="@+id/item_description" style="@style/normalText"></TextView> + + <Button android:text="@string/www" android:layout_width="wrap_content" + android:layout_height="wrap_content" android:id="@+id/item_button"></Button> + </LinearLayout> +</ScrollView> diff --git a/res/layout/start.xml b/res/layout/start.xml @@ -4,14 +4,14 @@ android:layout_height="fill_parent"> <LinearLayout android:orientation="horizontal" - android:layout_width="fill_parent" android:layout_height="wrap_content" android:padding="10sp"> + android:layout_width="fill_parent" android:layout_height="wrap_content" android:padding="14sp"> <ImageView android:src="@drawable/ibc_logo" android:padding="4sp" android:layout_width="wrap_content" android:layout_height="wrap_content"> </ImageView> - <Button android:id="@+id/forum" android:text="@string/forum" android:textSize="16sp" android:padding="10sp" - android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center_vertical|right"></Button> + <Button android:id="@+id/forum" android:text="@string/forum" android:textSize="20sp" android:padding="14sp" + android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="center"></Button> </LinearLayout> diff --git a/res/menu/forum_guest.xml b/res/menu/forum_guest.xml @@ -0,0 +1,5 @@ +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/menu_login" android:title="@string/login" + android:icon="@android:drawable/ic_lock_lock"></item> +</menu> + + \ No newline at end of file diff --git a/res/menu/topic_context.xml b/res/menu/topic_context.xml @@ -0,0 +1,5 @@ +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/menu_goto_top" android:title="@string/goto_first_post"></item> + <item android:id="@+id/menu_goto_bottom" android:title="@string/goto_last_post"></item> +</menu> + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml @@ -96,9 +96,14 @@ <string name="nousername">Es ist noch kein Benutzername konfiguriert</string> <string name="www">Zur Webseite</string> <string name="scroll_down">Zum Ende springen</string> + <string name="scroll_down_desc">Beim Laden von Themen automatisch zum letzten Beitrag springen</string> <string name="use_ibc_theme">Das IBC-Theme benutzen</string> <string name="auto_login">Automatisch anmelden</string> + <string name="auto_login_desc">Beim Starten des Forums automatisch anmelden.</string> <string name="num_load">Anzahl Posts laden</string> + <string name="num_load_desc">Anzahl der Themen/Beiträge, die initial geladen werden</string> + <string name="auto_load_next">Automatisch nachladen</string> + <string name="auto_load_next_desc">Beim Scrollen die nächsten Einträge automatisch laden</string> <string name="login">Anmelden</string> <string name="login_success">Benutzeranmeldung erfolgreich</string> <string name="login_failed">Benutzeranmeldung nicht erfolgreich</string> @@ -123,6 +128,7 @@ <string name="blog">Blog</string> <string name="links">Links</string> <string name="load_images">Bilder anzeigen</string> + <string name="load_images_desc">Automatisches Anzeigen von Bildern in Beiträgen. Dadurch wird die App langsamer und der Datenverbrauch steigt.</string> <string name="goto_first_post">Zum ersten Beitrag</string> <string name="goto_last_post">Zum letzten Beitrag</string> <string name="goto_top">Nach oben</string> @@ -130,4 +136,5 @@ <string name="reply">Antworten</string> <string name="new_topic">Neues Thema</string> <string name="parse_bbcode">BB-Code auswerten</string> + <string name="parse_bbcode_desc">In Forumbeiträgen den BB-Code auflösen. Kann die Anzeige von Beiträgen stark verlangsamen.</string> </resources> diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml @@ -3,7 +3,7 @@ android:title="@string/preferences"> <CheckBoxPreference android:defaultValue="true" - android:key="ibc_theme" android:title="@string/use_ibc_theme" /> + android:key="ibc_theme" android:title="@string/use_ibc_theme" android:enabled="false" /> <!-- <EditTextPreference android:key="timeout" @@ -14,31 +14,42 @@ <EditTextPreference android:key="server" android:title="@string/server"></EditTextPreference> --> + + <PreferenceCategory android:title="@string/username"> <EditTextPreference android:key="username" android:title="@string/username"></EditTextPreference> <EditTextPreference android:key="password" android:password="true" android:title="@string/password"></EditTextPreference> - - <CheckBoxPreference android:defaultValue="true" - android:key="auto_login" android:title="@string/auto_login" android:dependency="username" /> - + + <CheckBoxPreference android:defaultValue="false" + android:summary="@string/auto_login_desc" android:key="auto_login" + android:title="@string/auto_login" android:dependency="username" /> + </PreferenceCategory> <PreferenceCategory android:title="@string/forum"> <ListPreference android:defaultValue="10" - android:entries="@array/num_load_list" android:entryValues="@array/num_load_list" - android:key="num_load" android:title="@string/num_load" /> + android:summary="@string/num_load_desc" android:entries="@array/num_load_list" + android:entryValues="@array/num_load_list" android:key="num_load" + android:title="@string/num_load" /> + + <CheckBoxPreference android:defaultValue="true" + android:summary="@string/auto_load_next_desc" android:key="auto_load_next" + android:title="@string/auto_load_next" android:enabled="false" /> <CheckBoxPreference android:defaultValue="false" - android:key="scroll_down" android:title="@string/scroll_down" /> + android:summary="@string/scroll_down_desc" android:key="scroll_down" + android:title="@string/scroll_down" /> - <CheckBoxPreference android:defaultValue="true" - android:key="parse_bbcode" android:title="@string/parse_bbcode" /> - - <CheckBoxPreference android:defaultValue="true" - android:key="load_images" android:title="@string/load_images" android:dependency="parse_bbcode" /> + <CheckBoxPreference android:defaultValue="false" + android:summary="@string/parse_bbcode_desc" android:key="parse_bbcode" + android:title="@string/parse_bbcode" /> + + <CheckBoxPreference android:defaultValue="false" + android:key="load_images" android:summary="@string/load_images_desc" + android:title="@string/load_images" android:dependency="parse_bbcode" /> </PreferenceCategory> </PreferenceScreen> diff --git a/src/de/mtbnews/android/NewsDetailActivity.java b/src/de/mtbnews/android/NewsDetailActivity.java @@ -6,6 +6,9 @@ import java.net.URL; import org.mcsoxford.rss.RSSItem; +import de.mtbnews.android.image.ImageGetter; +import de.mtbnews.android.image.ImageGetterAsyncTask; + import android.app.Activity; import android.content.Intent; import android.content.SharedPreferences; @@ -44,7 +47,7 @@ public class NewsDetailActivity extends Activity final TextView desc = (TextView) findViewById(R.id.item_description); // if (e.getContent() != null) - final String html = item.getContent(); + final String html = item.getFullContent(); ImageGetter imageGetter = null; if (prefs.getBoolean("load_images", false)) @@ -70,45 +73,5 @@ public class NewsDetailActivity extends Activity } - protected class ImageGetter implements Html.ImageGetter - { - - public Drawable getDrawable(String source) - { - Drawable d = null; - String imageSource; - // if (!source.startsWith("http://www")) - // { - // imageSource = "http://www.minhembio.com" + source; - // } - // else - // { - imageSource = source; - // } - - try - { - URL myFileUrl = new URL(imageSource); - - HttpURLConnection conn = (HttpURLConnection) myFileUrl - .openConnection(); - conn.setDoInput(true); - conn.connect(); - InputStream is = conn.getInputStream(); - BitmapDrawable a = new BitmapDrawable(is); - d = a.getCurrent(); - d - .setBounds(0, 0, d.getIntrinsicWidth(), d - .getIntrinsicHeight()); - } - catch (Exception e) - { - throw new RuntimeException(e); - // d = null; - } - - return d; - } - }; } diff --git a/src/de/mtbnews/android/adapter/ListEntryContentAdapter.java b/src/de/mtbnews/android/adapter/ListEntryContentAdapter.java @@ -7,7 +7,9 @@ import java.util.List; import ru.perm.kefir.bbcode.BBProcessorFactory; import ru.perm.kefir.bbcode.TextProcessor; +import android.app.Activity; import android.content.Context; +import android.content.SharedPreferences; import android.text.Html; import android.text.format.DateFormat; import android.view.LayoutInflater; @@ -15,7 +17,9 @@ import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.TextView; +import de.mtbnews.android.IBCApplication; import de.mtbnews.android.R; +import de.mtbnews.android.image.ImageGetter; import de.mtbnews.android.tapatalk.wrapper.ListEntry; /** @@ -123,11 +127,26 @@ public class ListEntryContentAdapter extends BaseAdapter if (e.getContent() != null) { - // if ( prefs parse_bbcode ) {} - // TextProcessor create = BBProcessorFactory.getInstance().create(); - // CharSequence html = create.process("[b]..."); - // Html.fromHtml(html); - viewHolder.desc.setText(e.getContent()); + SharedPreferences prefs = ((IBCApplication) ((Activity) mContext) + .getApplication()).prefs; + if (prefs.getBoolean("parse_bbcode", false)) + { + + TextProcessor create = BBProcessorFactory.getInstance() + .create(); + CharSequence html = create.process(e.getContent()); + + ImageGetter imageGetter = null; + if (prefs.getBoolean("load_images", false)) + imageGetter = new ImageGetter(); + + viewHolder.desc.setText(Html.fromHtml(html.toString(), + imageGetter, null)); + } + else + { + viewHolder.desc.setText(e.getContent()); + } } else viewHolder.desc.setText(""); diff --git a/src/de/mtbnews/android/image/ImageGetter.java b/src/de/mtbnews/android/image/ImageGetter.java @@ -0,0 +1,52 @@ +package de.mtbnews.android.image; + +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.text.Html; + +/** + * @author dankert + * + */ +public class ImageGetter implements Html.ImageGetter +{ + + public Drawable getDrawable(String source) + { + Drawable d = null; + String imageSource; + // if (!source.startsWith("http://www")) + // { + // imageSource = "http://www.minhembio.com" + source; + // } + // else + // { + imageSource = source; + // } + + try + { + URL myFileUrl = new URL(imageSource); + + HttpURLConnection conn = (HttpURLConnection) myFileUrl + .openConnection(); + conn.setDoInput(true); + conn.connect(); + InputStream is = conn.getInputStream(); + BitmapDrawable a = new BitmapDrawable(is); + d = a.getCurrent(); + d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); + } + catch (Exception e) + { + throw new RuntimeException(e); + // d = null; + } + + return d; + } +}+ \ No newline at end of file diff --git a/src/org/mcsoxford/rss/RSSHandler.java b/src/org/mcsoxford/rss/RSSHandler.java @@ -128,7 +128,7 @@ class RSSHandler extends org.xml.sax.helpers.DefaultHandler { @Override public void set(String content) { if (item != null) { - item.setContent(content); + item.setFullContent(content); } } }; diff --git a/src/org/mcsoxford/rss/RSSItem.java b/src/org/mcsoxford/rss/RSSItem.java @@ -48,12 +48,12 @@ public class RSSItem extends RSSBase { * Returns the value of the optional &lt;content:encoded&gt; tag * @return string value of the element data */ - public String getContent() { + public String getFullContent() { return content; } /* Internal method for RSSHandler */ - void setContent(String content) { + void setFullContent(String content) { this.content = content; } } diff --git a/src/ru/perm/kefir/bbcode/AbstractCode.java b/src/ru/perm/kefir/bbcode/AbstractCode.java @@ -0,0 +1,71 @@ +package ru.perm.kefir.bbcode; + +import java.io.IOException; + +/** + * Abstract bb-code + * + * @author Vitaliy Samolovskih aka Kefir + */ +public abstract class AbstractCode implements Comparable<AbstractCode> { + protected static final int DEFAULT_PRIORITY = 0; + + /** + * Priority. If priority higher then code be checking early. + */ + private final int priority; + + /** + * The code name. + */ + private final String name; + + /** + * template for build result char sequence + */ + protected final Template template; + + /** + * Abstract constructor for the bb-code with priority + * + * @param template template + * @param name name of code + * @param priority priority. If priority higher then code be checking early. + */ + protected AbstractCode(Template template, String name, int priority) { + this.template = template; + this.priority = priority; + this.name = name; + } + + /** + * Process code: parse source and generate result string + * + * @param context current context + * @return true if next sequence in source is valid code + * @throws IOException append result to target + */ + public abstract boolean process(Context context) throws IOException; + + /** + * @param source source of text + * @return true if next sequence can be this code + */ + public abstract boolean suspicious(Source source); + + /** + * Compare by priorities + */ + public int compareTo(AbstractCode code) { + return this.priority - code.priority; + } + + /** + * Get code name + * + * @return code name + */ + public String getName() { + return name; + } +} diff --git a/src/ru/perm/kefir/bbcode/BBProcessor.java b/src/ru/perm/kefir/bbcode/BBProcessor.java @@ -0,0 +1,114 @@ +package ru.perm.kefir.bbcode; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; + +/** + * The bbcode processor. You can use the standard code set or define other. + * + * @author Kefir + */ +final class BBProcessor extends TextProcessorAdapter { + /** + * BB-codes + */ + private Scope scope = null; + private Template prefix = null; + private Template suffix = null; + private Map<String, Object> params = null; + + /** + * Create the bbcode processor + */ + BBProcessor() { + } + + /** + * Process bbcodes <br/> + * 1. Escape the xml special symbols<br/> + * 2. replace bbcodes to HTML-tags<br/> + * 3. replace symbols \r\n to HTML-tag "&lt;br/&gt;"<br/> + * + * @param source the source string + * @return result string + * @see TextProcessor#process(CharSequence) + */ + public CharSequence process(CharSequence source){ + Context context = new Context(); + StringBuilder target = new StringBuilder(); + context.setTarget(target); + context.setSource(new Source(source)); + context.setScope(scope); + if (params != null) { + for (Map.Entry<String, Object> entry : params.entrySet()) { + context.setAttribute(entry.getKey(), entry.getValue()); + } + } + + try { + prefix.generate(context); + context.parse(); + suffix.generate(context); + } catch (IOException e) { + // Never because StringBuilder not throw IOException + } + + return target; + } + + /** + * Set the root scope of text processor. + * + * @param scope root code scope + * @throws IllegalStateException if scope already setted + */ + void setScope(Scope scope) throws IllegalStateException { + if (this.scope == null) { + this.scope = scope; + } else { + throw new IllegalStateException("Can't change the root scope."); + } + } + + /** + * Set the prefix for text processor + * + * @param prefix template wich uses to create prefix + * @throws IllegalStateException If prefix already setted + */ + void setPrefix(Template prefix) throws IllegalStateException { + if (this.prefix == null) { + this.prefix = prefix; + } else { + throw new IllegalStateException("Can't change the prefix."); + } + } + + /** + * Set the suffix for text processor + * + * @param suffix template wich uses to create prefix + * @throws IllegalStateException If suffix already setted + */ + void setSuffix(Template suffix) { + if (this.suffix == null) { + this.suffix = suffix; + } else { + throw new IllegalStateException("Can't change the suffix."); + } + } + + /** + * Set text processor parameters map. + * + * @param params parameters + */ + void setParams(Map<String, Object> params) { + if (this.params == null) { + this.params = Collections.unmodifiableMap(params); + } else { + throw new IllegalStateException("Can't change parameters."); + } + } +} diff --git a/src/ru/perm/kefir/bbcode/BBProcessorFactory.java b/src/ru/perm/kefir/bbcode/BBProcessorFactory.java @@ -0,0 +1,86 @@ +package ru.perm.kefir.bbcode; + +import java.io.File; +import java.io.InputStream; + +/** + * Factory for creating BBProcessor from Stream, File, Resource with configuration or default bb-processor. + * + * @author Kefir + */ +public final class BBProcessorFactory implements TextProcessorFactory { + /** + * Instance of this class. See the Singleton pattern + */ + private static final BBProcessorFactory instance = new BBProcessorFactory(); + private final ConfigurationFactory configurationFactory = ConfigurationFactory.getInstance(); + + /** + * Return instance of BBProcessorFactory + * + * @return factory instance + */ + public static BBProcessorFactory getInstance() { + return instance; + } + + /** + * Private constructor. Because this is a singleton. + */ + private BBProcessorFactory() { + } + + /** + * Create the default bb-code processor. + * + * @return Default bb-code processor + * @throws TextProcessorFactoryException when can't read the default code set resource + */ + public TextProcessor create() { + return configurationFactory.create().create(); + } + + /** + * Create the bb-processor using xml-configuration resource + * + * @param resourceName name of resource file + * @return bb-code processor + * @throws TextProcessorFactoryException when can't find or read the resource or illegal config file + */ + public TextProcessor createFromResource(String resourceName) { + return configurationFactory.createFromResource(resourceName).create(); + } + + /** + * Create the bb-processor from XML InputStream + * + * @param stream the input stream with XML + * @return bb-code processor + * @throws TextProcessorFactoryException when can't build Document + */ + public TextProcessor create(InputStream stream) { + return configurationFactory.create(stream).create(); + } + + /** + * Create the bb-code processor from file with XML-configuration. + * + * @param file file with configuration + * @return bb-code processor + * @throws TextProcessorFactoryException any problems + */ + public TextProcessor create(File file) { + return configurationFactory.create(file).create(); + } + + /** + * Create the bb-code processor from file with XML-configuration. + * + * @param fileName name of file with configuration + * @return bb-code processor + * @throws TextProcessorFactoryException any problems + */ + public TextProcessor create(String fileName) { + return create(new File(fileName)); + } +} diff --git a/src/ru/perm/kefir/bbcode/Code.java b/src/ru/perm/kefir/bbcode/Code.java @@ -0,0 +1,72 @@ +package ru.perm.kefir.bbcode; + +import java.io.IOException; + +/** + * The bbcode class + * + * @author Kefir + */ +public class Code extends AbstractCode { + /** + * Pattern for parsing code + */ + private final Pattern pattern; + + /** + * Create bb-code with default name and zero priority. + * + * @param pattern parse pattern + * @param template building tamplate + */ + public Code(Pattern pattern, Template template) { + super(template, Util.generateRandomName(), DEFAULT_PRIORITY); + this.pattern = pattern; + } + + /** + * Create the bb-code with priority + * + * @param pattern pattern to parse the source text + * @param template template to build target text + * @param name name of code + * @param priority priority. If priority higher then code be checking early. + */ + public Code(Pattern pattern, Template template, String name, int priority) { + super(template, name, priority); + this.pattern = pattern; + } + + /** + * Parse bb-code + * + * Before invocation suspicious method must be call + * + * @param context the bb-processing context + * @return true - if parse source + * false - if can't parse code + * @throws java.io.IOException if can't append to target + */ + public boolean process(Context context) throws IOException { + Context codeContext = new Context(context); + if (pattern.parse(codeContext)) { + codeContext.mergeWithParent(); + template.generate(context); + return true; + } + + return false; + } + + /** + * Check if next sequence can be parsed with this code. + * It's most called method in this project. + * + * @param source text source + * @return true - if next sequence can be parsed with this code; + * false - only if next sequence can't be parsed with this code. + */ + public boolean suspicious(Source source) { + return pattern.suspicious(source); + } +} diff --git a/src/ru/perm/kefir/bbcode/Configuration.java b/src/ru/perm/kefir/bbcode/Configuration.java @@ -0,0 +1,138 @@ +package ru.perm.kefir.bbcode; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * Configuration of bbcode processor. + * It's thread safe class. + * + * @author Vitaliy Samolovskih aka Kefir + */ +public class Configuration implements TextProcessorFactory { + private Scope scope = null; + private Template prefix = Template.EMPTY; + private Template suffix = Template.EMPTY; + private final Map<String, Object> params = new HashMap<String, Object>(); + + /** + * Create the configuration + */ + public Configuration() { + } + + /** + * Create text processor. + * + * @return text processor + * @throws IllegalStateException if scope not setted + */ + public synchronized TextProcessor create() throws IllegalStateException { + if (scope == null) { + throw new IllegalStateException("Scope is null."); + } + BBProcessor processor = new BBProcessor(); + processor.setScope(scope); + processor.setPrefix(prefix); + processor.setSuffix(suffix); + processor.setParams(params); + return processor; + } + + /** + * Set root scope for text processor. + * + * @param scope root scope, text will be parse with this scope + */ + public synchronized void setScope(Scope scope) { + this.scope = scope; + } + + /** + * Set prefix template. Prefix append to start of processed text. + * + * @param prefix template for prefix + */ + public synchronized void setPrefix(Template prefix) { + if (prefix != null) { + this.prefix = prefix; + } else { + this.prefix = Template.EMPTY; + } + } + + /** + * Set suffix template. Suffix append to end of processed text. + * + * @param suffix template for suffix + */ + public synchronized void setSuffix(Template suffix) { + if (suffix != null) { + this.suffix = suffix; + } else { + this.suffix = Template.EMPTY; + } + } + + /** + * Add param with name <code>name</code> and value <code>value</code> to root context. + * Call addParam(String, object) + * + * @param name name of context parameter + * @param value value of context parameter + * @see #addParam(String, Object) + */ + public synchronized void setParam(String name, Object value) { + addParam(name, value); + } + + /** + * Add param with name <code>name</code> and value <code>value</code> to root context. + * + * @param name name of context parameter + * @param value value of context parameter + */ + public synchronized void addParam(String name, Object value) { + params.put(name, value); + } + + /** + * Add param from map to root context. + * + * @param params Map contained params + */ + public synchronized void addParams(Map<String, ?> params) { + this.params.putAll(params); + } + + /** + * Add param from properties to root context. + * + * @param properties Properties object + */ + public synchronized void addParams(Properties properties) { + for (Map.Entry<Object, Object> entry : properties.entrySet()) { + Object key = entry.getKey(); + if (key != null) { + this.params.put(key.toString(), entry.getValue()); + } + } + } + + /** + * Remove parameter with name <code>name</code> from context. + * + * @param name name of parameter + */ + public synchronized void removeParam(String name) { + this.params.remove(name); + } + + /** + * Remove all parameters from context. + */ + public synchronized void clearParams() { + this.params.clear(); + } +} diff --git a/src/ru/perm/kefir/bbcode/ConfigurationFactory.java b/src/ru/perm/kefir/bbcode/ConfigurationFactory.java @@ -0,0 +1,885 @@ +package ru.perm.kefir.bbcode; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.*; +import java.util.*; + +/** + * Create the text processor configuration + * + * @author Vitaliy Samolovskih aka Kefir + */ +public final class ConfigurationFactory +{ + // Helper constants + private static final String DEFAULT_CONFIGURATION = "ru/perm/kefir/bbcode/default"; + private static final String DEFAULT_USER_CONFIGURATION = "kefirbb"; + private static final String CONFIGURATION_EXTENSION = ".xml"; + + // Configuration paths + public static final String DEFAULT_USER_CONFIGURATION_FILE = DEFAULT_USER_CONFIGURATION + + CONFIGURATION_EXTENSION; + public static final String DEFAULT_CONFIGURATION_FILE = DEFAULT_CONFIGURATION + + CONFIGURATION_EXTENSION; + public static final String DEFAULT_PROPERTIES_FILE = "kefirbb.properties"; + public static final String DEFAULT_PROPERTIES_XML_FILE = "kefirbb.properties.xml"; + + /** + * Schema location + */ + private static final String SCHEMA_LOCATION = "http://kefir-bb.sourceforge.net/schemas"; + + /** + * Constants wich uses when parse XML-configuration + */ + private static final String TAG_CODE = "code"; + private static final String TAG_CODE_ATTR_NAME = "name"; + private static final String TAG_CODE_ATTR_PRIORITY = "priority"; + private static final String TAG_PATTERN = "pattern"; + private static final String TAG_VAR = "var"; + private static final String TAG_VAR_ATTR_NAME = "name"; + private static final String DEFAULT_VARIABLE_NAME = "variable"; + private static final String TAG_VAR_ATTR_PARSE = "parse"; + private static final boolean DEFAULT_PARSE_VALUE = true; + private static final String TAG_VAR_ATTR_INHERIT = "inherit"; + private static final boolean DEFAULT_INHERIT_VALUE = false; + private static final String TAG_VAR_ATTR_REGEX = "regex"; + private static final String TAG_VAR_ATTR_TRANSPARENT = "transparent"; + private static final String TAG_TEMPLATE = "template"; + private static final String TAG_SCOPE = "scope"; + private static final String TAG_SCOPE_ATTR_NAME = "name"; + private static final String TAG_SCOPE_ATTR_PARENT = "parent"; + private static final String TAG_SCOPE_ATTR_IGNORE_TEXT = "ignoreText"; + private static final boolean DEFAULT_IGNORE_TEXT = false; + private static final String TAG_CODEREF = "coderef"; + private static final String TAG_CODEREF_ATTR_NAME = TAG_CODE_ATTR_NAME; + private static final String TAG_PREFIX = "prefix"; + private static final String TAG_SUFFIX = "suffix"; + private static final String TAG_PARAMS = "params"; + private static final String TAG_PARAM = "param"; + private static final String TAG_PARAM_ATTR_NAME = "name"; + private static final String TAG_PARAM_ATTR_VALUE = "value"; + + /** + * Singletone class instance + */ + private static final ConfigurationFactory instance = new ConfigurationFactory(); + + /** + * private constructor + */ + private ConfigurationFactory() + { + } + + /** + * Return instance of class ConfigurationFactory + * + * @return configuration factory + */ + public static ConfigurationFactory getInstance() + { + return instance; + } + + /** + * Create the default bb-code processor. + * + * @return Default bb-code processor + * @throws ru.perm.kefir.bbcode.TextProcessorFactoryException + * when can't read the default code set resource + */ + public Configuration create() + { + Configuration configuration; + try + { + InputStream stream = null; + try + { + // Search the user configuration + stream = Util + .openResourceStream(DEFAULT_USER_CONFIGURATION_FILE); + + // If user configuration not found then use default + if (stream == null) + { + stream = Util + .openResourceStream(DEFAULT_CONFIGURATION_FILE); + } + + if (stream != null) + { + configuration = create(stream); + } + else + { + throw new TextProcessorFactoryException( + "Can't find or open resource."); + } + } + finally + { + if (stream != null) + { + stream.close(); + } + } + + Properties properties = new Properties(); + + // Load properties from .property file + InputStream propertiesStream = null; + try + { + propertiesStream = Util + .openResourceStream(DEFAULT_PROPERTIES_FILE); + if (propertiesStream != null) + { + properties.load(propertiesStream); + } + } + finally + { + if (propertiesStream != null) + { + propertiesStream.close(); + } + } + + // Load properties from xml file + InputStream xmlPropertiesStream = null; + try + { + xmlPropertiesStream = Util + .openResourceStream(DEFAULT_PROPERTIES_XML_FILE); + if (xmlPropertiesStream != null) + { + properties.loadFromXML(xmlPropertiesStream); + } + } + finally + { + if (xmlPropertiesStream != null) + { + xmlPropertiesStream.close(); + } + } + + configuration.addParams(properties); + } + catch (IOException e) + { + throw new TextProcessorFactoryException(e); + } + return configuration; + } + + /** + * Create the bb-processor using xml-configuration resource + * + * @param resourceName + * name of resource file + * @return bb-code processor + * @throws ru.perm.kefir.bbcode.TextProcessorFactoryException + * when can't find or read the resource or illegal config file + */ + public Configuration createFromResource(String resourceName) + { + if (resourceName == null) + { + throw new IllegalArgumentException( + "The resource name is not setted."); + } + + Configuration configuration; + try + { + InputStream stream = null; + try + { + stream = Util.openResourceStream(resourceName); + + if (stream != null) + { + configuration = create(stream); + } + else + { + throw new TextProcessorFactoryException( + "Can't find or open resource \"" + resourceName + + "\"."); + } + } + finally + { + if (stream != null) + { + stream.close(); + } + } + } + catch (IOException e) + { + throw new TextProcessorFactoryException(e); + } + + return configuration; + } + + /** + * Create the bb-code processor from file with XML-configuration. + * + * @param fileName + * name of file with configuration + * @return bb-code processor + * @throws ru.perm.kefir.bbcode.TextProcessorFactoryException + * any problems + */ + public Configuration create(String fileName) + { + return create(new File(fileName)); + } + + /** + * Create the bb-code processor from file with XML-configuration. + * + * @param file + * file with configuration + * @return bb-code processor + * @throws ru.perm.kefir.bbcode.TextProcessorFactoryException + * any problems + */ + public Configuration create(File file) + { + try + { + Configuration configuration; + InputStream stream = new BufferedInputStream(new FileInputStream( + file)); + try + { + configuration = create(stream); + } + finally + { + stream.close(); + } + return configuration; + } + catch (IOException e) + { + throw new TextProcessorFactoryException(e); + } + } + + /** + * Create the bb-processor from XML InputStream + * + * @param stream + * the input stream with XML + * @return bb-code processor + * @throws ru.perm.kefir.bbcode.TextProcessorFactoryException + * when can't build Document + */ + public Configuration create(InputStream stream) + { + try + { + DocumentBuilderFactory factory = DocumentBuilderFactory + .newInstance(); + factory.setValidating(false); + factory.setIgnoringElementContentWhitespace(true); + factory.setNamespaceAware(true); + DocumentBuilder documentBuilder = factory.newDocumentBuilder(); + Document document = documentBuilder.parse(stream); + return create(document); + } + catch (ParserConfigurationException e) + { + throw new TextProcessorFactoryException(e); + } + catch (IOException e) + { + throw new TextProcessorFactoryException(e); + } + catch (SAXException e) + { + throw new TextProcessorFactoryException(e); + } + } + + /** + * Create the bb-code processor from DOM Document + * + * @param dc + * document + * @return bb-code processor + * @throws ru.perm.kefir.bbcode.TextProcessorFactoryException + * If invalid Document + */ + private Configuration create(Document dc) + { + // Create configuration + Configuration configuration = new Configuration(); + + // Parse parameters + configuration.addParams(parseParams(dc)); + + // Parse prefix and suffix + configuration.setPrefix(parseFix(dc, TAG_PREFIX)); + configuration.setSuffix(parseFix(dc, TAG_SUFFIX)); + + // Parse codes and scope and set this to configuration + // Parse scopes + NodeList scopeNodeList = dc.getDocumentElement() + .getElementsByTagNameNS(SCHEMA_LOCATION, TAG_SCOPE); + Map<String, Scope> scopes = parseScopes(scopeNodeList); + + boolean fillRoot = false; + Scope root; + if (!scopes.containsKey(Scope.ROOT)) + { + root = new Scope(Scope.ROOT); + scopes.put(Scope.ROOT, root); + fillRoot = true; + } + else + { + root = scopes.get(Scope.ROOT); + } + + // Parse codes + Map<String, AbstractCode> codes = parseCodes(dc, scopes); + + // include codes in scopes + fillScopeCodes(scopeNodeList, scopes, codes); + + // If root scope not defined in configuration file, then root scope + // fills all codes + if (fillRoot) + { + root.setScopeCodes(new HashSet<AbstractCode>(codes.values())); + } + + // set root scope + configuration.setScope(root); + + // return configuration + return configuration; + } + + private Map<String, String> parseParams(Document dc) + { + Map<String, String> params = new HashMap<String, String>(); + NodeList paramsElements = dc.getElementsByTagName(TAG_PARAMS); + if (paramsElements.getLength() > 0) + { + Element paramsElement = (Element) paramsElements.item(0); + NodeList paramElements = paramsElement + .getElementsByTagName(TAG_PARAM); + for (int i = 0; i < paramElements.getLength(); i++) + { + Node paramElement = paramElements.item(i); + String name = nodeAttribute(paramElement, TAG_PARAM_ATTR_NAME, + ""); + String value = nodeAttribute(paramElement, + TAG_PARAM_ATTR_VALUE, ""); + if (name != null && name.length() > 0) + { + params.put(name, value); + } + } + } + return params; + } + + @SuppressWarnings( { "unchecked" }) + private Template parseFix(Document dc, String tagname) + { + Template fix; + NodeList prefixElementList = dc.getElementsByTagName(tagname); + if (prefixElementList.getLength() > 0) + { + fix = parseTemplate(prefixElementList.item(0)); + } + else + { + fix = Template.EMPTY; + } + return fix; + } + + /** + * Fill codes of scopes. + * + * @param scopeNodeList + * node list with scopes definitions + * @param scopes + * scopes + * @param codes + * codes + * @throws TextProcessorFactoryException + * any problem + */ + private void fillScopeCodes(NodeList scopeNodeList, + Map<String, Scope> scopes, Map<String, AbstractCode> codes) + { + int initCount; + int notInitCount; + do + { + initCount = 0; + notInitCount = 0; + for (int i = 0; i < scopeNodeList.getLength(); i++) + { + Element scopeElement = (Element) scopeNodeList.item(i); + Scope scope = scopes.get(scopeElement + .getAttribute(TAG_SCOPE_ATTR_NAME)); + if (!scope.isInitialized()) + { + // parent + boolean canBeInit = true; + if (scopeElement.hasAttribute(TAG_SCOPE_ATTR_PARENT)) + { + String parentName = scopeElement + .getAttribute(TAG_SCOPE_ATTR_PARENT); + Scope parent = scopes.get(parentName); + if (parent == null) + { + throw new TextProcessorFactoryException( + "Can't find parent scope \"" + parentName + + "\"."); + } + if (parent.isInitialized()) + { + scope.setParent(parent); + } + else + { + canBeInit = false; + } + } + + if (canBeInit) + { + // Add codes to scope + Set<AbstractCode> scopeCodes = new HashSet<AbstractCode>(); + + // bind exists codes + NodeList coderefs = scopeElement + .getElementsByTagNameNS(SCHEMA_LOCATION, + TAG_CODEREF); + for (int j = 0; j < coderefs.getLength(); j++) + { + Element ref = (Element) coderefs.item(j); + String codeName = ref + .getAttribute(TAG_CODEREF_ATTR_NAME); + AbstractCode code = codes.get(codeName); + if (code == null) + { + throw new TextProcessorFactoryException( + "Can't find code \"" + codeName + "\"."); + } + scopeCodes.add(code); + } + + // Add inline codes + NodeList inlineCodes = scopeElement + .getElementsByTagNameNS(SCHEMA_LOCATION, + TAG_CODE); + for (int j = 0; j < inlineCodes.getLength(); j++) + { + // Inline element code + Element ice = (Element) inlineCodes.item(j); + scopeCodes.add(parseCode(ice, scopes)); + } + + // Set codes to scope + scope.setScopeCodes(scopeCodes); + initCount++; + } + else + { + notInitCount++; + } + } + } + } + while (initCount > 0 && notInitCount > 0); + + if (notInitCount > 0) + { + throw new TextProcessorFactoryException("Can't init scopes."); + } + } + + /** + * Parse scopes from XML + * + * @param scopeNodeList + * list with scopes definitions + * @return scopes + * @throws TextProcessorFactoryException + * any problems + */ + private Map<String, Scope> parseScopes(NodeList scopeNodeList) + { + Map<String, Scope> scopes = new HashMap<String, Scope>(); + for (int i = 0; i < scopeNodeList.getLength(); i++) + { + Element scopeElement = (Element) scopeNodeList.item(i); + String name = scopeElement.getAttribute(TAG_SCOPE_ATTR_NAME); + if (name.length() == 0) + { + throw new TextProcessorFactoryException("Illegal scope name."); + } + Scope scope = new Scope(name); + scope.setIgnoreText(nodeAttribute(scopeElement, + TAG_SCOPE_ATTR_IGNORE_TEXT, DEFAULT_IGNORE_TEXT)); + scopes.put(scope.getName(), scope); + } + return scopes; + } + + /** + * Parse codes from XML + * + * @param dc + * DOM document with configuration + * @param scopes + * scope set + * @return codes + * @throws TextProcessorFactoryException + * any problem + */ + private Map<String, AbstractCode> parseCodes(Document dc, + Map<String, Scope> scopes) + { + Map<String, AbstractCode> codes = new HashMap<String, AbstractCode>(); + NodeList codeNodeList = dc.getDocumentElement().getElementsByTagNameNS( + SCHEMA_LOCATION, TAG_CODE); + for (int i = 0; i < codeNodeList.getLength(); i++) + { + AbstractCode code = parseCode((Element) codeNodeList.item(i), + scopes); + codes.put(code.getName(), code); + } + return codes; + } + + /** + * Parse bb-code from DOM Node + * + * @param codeElement + * node, represent code wich + * @param scopes + * mapping names of scope to scope + * @return bb-code + * @throws ru.perm.kefir.bbcode.TextProcessorFactoryException + * if error format + */ + private AbstractCode parseCode(Element codeElement, + Map<String, Scope> scopes) + { + // Code name + String name = nodeAttribute(codeElement, TAG_CODE_ATTR_NAME, Util + .generateRandomName()); + + // Code priority + int priority = nodeAttribute(codeElement, TAG_CODE_ATTR_PRIORITY, + AbstractCode.DEFAULT_PRIORITY); + + // Template to building + Template template; + NodeList templateElements = codeElement.getElementsByTagNameNS( + SCHEMA_LOCATION, TAG_TEMPLATE); + if (templateElements.getLength() > 0) + { + template = parseTemplate(templateElements.item(0)); + } + else + { + throw new TextProcessorFactoryException( + "Illegal configuration. Can't find template of code."); + } + + // Pattern to parsing + NodeList patternElements = codeElement.getElementsByTagNameNS( + SCHEMA_LOCATION, TAG_PATTERN); + if (patternElements.getLength() <= 0) + { + throw new TextProcessorFactoryException( + "Illegal configuration. Can't find pattern of code."); + } + + AbstractCode code = null; + + // Attempt parse constant code + NodeList patternList = patternElements.item(0).getChildNodes(); + if (patternList.getLength() == 1) + { + Node node = patternList.item(0); + if (node.getNodeType() == Node.TEXT_NODE) + { + code = new ConstantCode(node.getNodeValue(), template, name, + priority); + } + } + + if (code == null) + { + // Create code + code = new Code(parsePattern(patternElements.item(0), scopes), + template, name, priority); + } + + // return code + return code; + } + + /** + * Parse code pattern for parse text. + * + * @param node + * pattern node with pattern description + * @param scopes + * mapping names of scope to scope + * @return list of pattern elements + * @throws TextProcessorFactoryException + * If invalid pattern format + */ + private Pattern parsePattern(Node node, Map<String, Scope> scopes) + { + List<PatternElement> elements = new LinkedList<PatternElement>(); + NodeList patternList = node.getChildNodes(); + int patternLength = patternList.getLength(); + if (patternLength <= 0) + { + throw new TextProcessorFactoryException("Invalid pattern"); + } + for (int k = 0; k < patternLength; k++) + { + Node el = patternList.item(k); + if (el.getNodeType() == Node.TEXT_NODE) + { + elements.add(new Constant(el.getNodeValue())); + } + else if (el.getNodeType() == Node.ELEMENT_NODE + && el.getLocalName().equals(TAG_VAR) + && (k != 0 || nodeHasAttribute(el, TAG_VAR_ATTR_REGEX))) + { + elements.add(parseNamedElement(el, scopes)); + } + else if (el.getNodeType() == Node.ENTITY_REFERENCE_NODE) + { + elements.add(new Constant("?")); + } + else + { + throw new TextProcessorFactoryException("Invalid pattern, Nodetype="+el.getNodeType()); + } + } + return new Pattern(elements); + } + + private PatternElement parseNamedElement(Node el, Map<String, Scope> scopes) + { + String name = nodeAttribute(el, TAG_VAR_ATTR_NAME, + DEFAULT_VARIABLE_NAME); + PatternElement namedElement; + if (nodeAttribute(el, TAG_VAR_ATTR_PARSE, DEFAULT_PARSE_VALUE) + && !nodeHasAttribute(el, TAG_VAR_ATTR_REGEX)) + { + namedElement = parseText(el, name, scopes); + } + else + { + namedElement = parseVariable(el, name); + } + return namedElement; + } + + private Text parseText(Node el, String name, Map<String, Scope> scopes) + { + Text text; + if (nodeAttribute(el, TAG_VAR_ATTR_INHERIT, DEFAULT_INHERIT_VALUE)) + { + text = new Text(name, nodeAttribute(el, TAG_VAR_ATTR_TRANSPARENT, + false)); + } + else + { + text = new Text(name, scopes.get(nodeAttribute(el, TAG_SCOPE, + Scope.ROOT)), nodeAttribute(el, TAG_VAR_ATTR_TRANSPARENT, + false)); + } + return text; + } + + private Variable parseVariable(Node el, String name) + { + Variable variable; + if (nodeHasAttribute(el, TAG_VAR_ATTR_REGEX)) + { + variable = new Variable(name, java.util.regex.Pattern + .compile(nodeAttribute(el, TAG_VAR_ATTR_REGEX))); + } + else + { + variable = new Variable(name); + } + return variable; + } + + /** + * Parse template fo generate text. + * + * @param node + * template node + * @return list of template elements + */ + private Template parseTemplate(Node node) + { + List<TemplateElement> elements = new LinkedList<TemplateElement>(); + NodeList templateList = node.getChildNodes(); + for (int k = 0; k < templateList.getLength(); k++) + { + Node el = templateList.item(k); + if (el.getNodeType() == Node.ELEMENT_NODE + && el.getLocalName().equals(TAG_VAR)) + { + elements.add(new NamedValue(nodeAttribute(el, + TAG_VAR_ATTR_NAME, DEFAULT_VARIABLE_NAME))); + } + else + { + elements.add(new Constant(el.getNodeValue())); + } + } + return new Template(elements); + } + + /** + * Return node attribute value, if exists or default attibute value + * + * @param node + * XML-node + * @param attributeName + * attributeName + * @param defaultValue + * attribute default value + * @return attribute value or default value + */ + private boolean nodeAttribute(Node node, String attributeName, + boolean defaultValue) + { + boolean value = defaultValue; + if (node.hasAttributes()) + { + Node attribute = node.getAttributes().getNamedItem(attributeName); + if (attribute != null) + { + value = Boolean.valueOf(attribute.getNodeValue()); + } + } + return value; + } + + /** + * Return node attribute value, if exists or default attibute value + * + * @param node + * XML-node + * @param attributeName + * attributeName + * @param defaultValue + * attribute default value + * @return attribute value or default value + */ + private String nodeAttribute(Node node, String attributeName, + String defaultValue) + { + String value = defaultValue; + if (node.hasAttributes()) + { + Node attribute = node.getAttributes().getNamedItem(attributeName); + if (attribute != null) + { + value = attribute.getNodeValue(); + } + } + return value; + } + + /** + * Return node attribute value, if exists or null value + * + * @param node + * XML-node + * @param attributeName + * attributeName + * @return attribute value or default value + */ + private String nodeAttribute(Node node, String attributeName) + { + String value = null; + if (node.hasAttributes()) + { + Node attribute = node.getAttributes().getNamedItem(attributeName); + if (attribute != null) + { + value = attribute.getNodeValue(); + } + } + return value; + } + + /** + * Return node attribute value, if exists or default attibute value + * + * @param node + * XML-node + * @param attributeName + * attributeName + * @param defaultValue + * attribute default value + * @return attribute value or default value + */ + private int nodeAttribute(Node node, String attributeName, int defaultValue) + { + int value = defaultValue; + if (node.hasAttributes()) + { + Node attribute = node.getAttributes().getNamedItem(attributeName); + if (attribute != null) + { + value = Integer.decode(attribute.getNodeValue()); + } + } + return value; + } + + /** + * Check node attribute. + * + * @param node + * XML-node + * @param attributeName + * name of attribute + * @return true if node has attribute with specified name false if has not + */ + private boolean nodeHasAttribute(Node node, String attributeName) + { + return node.hasAttributes() + && node.getAttributes().getNamedItem(attributeName) != null; + } +} diff --git a/src/ru/perm/kefir/bbcode/Constant.java b/src/ru/perm/kefir/bbcode/Constant.java @@ -0,0 +1,91 @@ +package ru.perm.kefir.bbcode; + +/** + * Constant element of pattern or template + * + * @author Kefir + */ +public class Constant implements PatternElement, TemplateElement { + /** + * Constant value + */ + private final String value; + + /** + * First char of constant. It need for better performance. + */ + private final char firstChar; + + /** + * Length of constant value + */ + private final int valueLength; + + /** + * Create constant element. + * + * @param value constant value + */ + public Constant(String value) { + this.value = value; + this.valueLength = value.length(); + this.firstChar = value.charAt(0); + } + + /** + * Parse constant + * + * @param context current context + * @param terminator not used + * @return true - if next sequence in source equals to this constant value, + * false - other + */ + public boolean parse(Context context, PatternElement terminator) { + if (isNextIn(context.getSource())) { + context.getSource().incOffset(valueLength); + return true; + } else { + return false; + } + } + + /** + * Return constant value + * + * @param context context. Not used. + */ + public CharSequence generate(Context context) { + return value; + } + + /** + * Check equals next sequence in source to this constant + * + * @param source source text + * @return true if next subsequence is equals + * false other + */ + public boolean isNextIn(Source source) { + return firstChar == source.current() + && source.hasNext(valueLength) + && value.contentEquals(source.subTo(valueLength)); + } + + /** + * Find this constant. + * + * @param source text source + * @return смещение константы + */ + public int findIn(Source source) { + return source.find(value); + } + + /** + * @return string representation of this object. + */ + @Override + public String toString() { + return "constant:" + value; + } +} diff --git a/src/ru/perm/kefir/bbcode/ConstantCode.java b/src/ru/perm/kefir/bbcode/ConstantCode.java @@ -0,0 +1,44 @@ +package ru.perm.kefir.bbcode; + +import java.io.IOException; + +/** + * Code with constant string pattern. For basic escaping. + * + * @author Vitaliy Samolovskih aka Kefir + */ +public class ConstantCode extends AbstractCode { + private final String value; + private final char firstChar; + private final int valueLength; + + /** + * Create bb-code with constant pattern + * + * @param value pattern value + * @param template template + * @param name name of code + * @param priority priority. If priority higher then code be checking early. + */ + public ConstantCode(String value, Template template, String name, int priority) { + super(template, name, priority); + + this.value = value; + this.firstChar = value.charAt(0); + this.valueLength = value.length(); + } + + @Override + public boolean process(Context context) throws IOException { + context.getSource().incOffset(valueLength); + template.generate(context); + return true; + } + + @Override + public boolean suspicious(Source source) { + return firstChar == source.current() + && source.hasNext(valueLength) + && value.contentEquals(source.subTo(valueLength)); + } +} diff --git a/src/ru/perm/kefir/bbcode/Context.java b/src/ru/perm/kefir/bbcode/Context.java @@ -0,0 +1,248 @@ +package ru.perm.kefir.bbcode; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * The bb-processing context + * + * @author Kefir + */ +public class Context { + /** + * Parent context + */ + private final Context parent; + + /** + * source text + */ + private Source source; + + /** + * Target builder + */ + private Appendable target = null; + + /** + * Text terminator,this mark stop text processing + */ + private PatternElement terminator = null; + + /** + * Code scope + */ + private Scope scope; + + /** + * Codes array for performance + */ + private AbstractCode[] codes = new AbstractCode[0]; + + /** + * Context attributes + */ + private final Map<String, Object> attributes = new HashMap<String, Object>(); + + /** + * тэги с ошибками. т.е. позиции в которых тэги с ошибками + */ + private final Map<Scope, IntSet> falseMemo; + private IntSet scopeFalseMemo; + + /** + * Default constructor + */ + public Context() { + parent = null; + falseMemo = new HashMap<Scope, IntSet>(); + } + + /** + * Constructor of child-context + * + * @param parent parent context + */ + public Context(Context parent) { + this.parent = parent; + this.source = parent.source; + this.target = parent.target; + this.falseMemo = parent.falseMemo; + this.terminator = parent.terminator; + + this.setScope(parent.scope); + } + + /** + * Парсит тект с BB-кодами + * + * @throws java.io.IOException if can't append chars to target + */ + public void parse() throws IOException { + while (hasNextAdjustedForTerminator()) { + if (!process()) { + if (scope.isIgnoreText()) { + source.incOffset(); + } else { + getTarget().append(source.next()); + } + } + } + } + + /** + * Обрабатывает BB-коды + * + * @return true если найден BB-код + * @throws java.io.IOException if can't append to target + */ + private boolean process() throws IOException { + int offset = source.getOffset(); + if (checkBadTag(offset)) { + return false; + } + + boolean suspicious = false; + boolean parsed = false; + for (AbstractCode code : codes) { + if (code.suspicious(source)) { + suspicious = true; + if (code.process(this)) { + parsed = true; + break; + } + } + } + + if (suspicious && !parsed) { + addBadTag(offset); + } + + return parsed; + } + + /** + * Add the bag tag position + * + * @param offset offset of bad tag in source + */ + private void addBadTag(int offset) { + scopeFalseMemo.add(offset); + } + + /** + * Check the bag tag + * + * @param offset offset of tag + * @return true if at this ofsset tag is bad + */ + private boolean checkBadTag(int offset) { + return scopeFalseMemo.contains(offset); + } + + /** + * Check has chars in source before terminator or not + * + * @return true if chars exists + * false if chars canceled + */ + private boolean hasNextAdjustedForTerminator() { + return source.hasNext() && (terminator == null || !terminator.isNextIn(source)); + } + + /** + * Put all local attributes to parent context + */ + public void mergeWithParent() { + parent.attributes.putAll(this.attributes); + } + + /** + * Add or set context attribute + * + * @param name attribute name + * @param value attribute value + */ + public void setAttribute(String name, Object value) { + attributes.put(name, value); + } + + /** + * Get the context attribute. If attribute not exists in current context, + * then context search the sttribute in parent context + * + * @param name attribute name + * @return attribute value + */ + public Object getAttribute(String name) { + Object value = getLocalAttribute(name); + if (value == null && parent != null) { + value = parent.getAttribute(name); + } + return value; + } + + /** + * Return attribute from this context, not parent + * + * @param name attribute name + * @return attribute value + */ + public Object getLocalAttribute(String name) { + return attributes.get(name); + } + + /** + * Set list of codes in current context + * + * @param scope code scope + */ + public void setScope(Scope scope) { + this.scope = scope; + + // codes + List<AbstractCode> codeList = scope.getCodes(); + codes = codeList.toArray(new AbstractCode[codeList.size()]); + + // Scope false memo + scopeFalseMemo = falseMemo.get(scope); + if (scopeFalseMemo == null) { + scopeFalseMemo = new IntSet(); + falseMemo.put(scope, scopeFalseMemo); + } + } + + public Source getSource() { + return source; + } + + public void setSource(Source source) { + this.source = source; + } + + public Appendable getTarget() { + return target; + } + + public void setTarget(Appendable target) { + this.target = target; + } + + /** + * get Text terminator,this mark stop text processing + * + * @return terminator + */ + public PatternElement getTerminator() { + return terminator; + } + + /** + * @param terminator Text terminator,this mark stop text processing + */ + public void setTerminator(PatternElement terminator) { + this.terminator = terminator; + } +} diff --git a/src/ru/perm/kefir/bbcode/EscapeProcessor.java b/src/ru/perm/kefir/bbcode/EscapeProcessor.java @@ -0,0 +1,86 @@ +package ru.perm.kefir.bbcode; + +import java.util.Arrays; + +/** + * Class for escape processing. For example, EscapeXmlProcessorFactory use this class for create EscapeXmlProcessor. + * + * @author Kefir + */ +public class EscapeProcessor extends TextProcessorAdapter { + /** + * Escape symbols with replacement. + */ + private final String[][] escape; + + /** + * Construct the escape processor with special escape symbols. + * + * @param escape escape symbols with replacement. This is a array of array wich consist of two strings + * the pattern and the replacement + */ + public EscapeProcessor(String[][] escape) { + this.escape = escape; + } + + /** + * Process the text + * + * @param source the sourcetext + * @return the result of text processing + * @see TextProcessor#process(CharSequence) + */ + public CharSequence process(CharSequence source) { + StringBuilder result = new StringBuilder(); + if (source != null && source.length() > 0) { + String stringSource; + if (source instanceof String) { + stringSource = (String) source; + } else { + stringSource = source.toString(); + } + + // Array to cache founded indexes of sequences + int[] indexes = new int[escape.length]; + Arrays.fill(indexes, -1); + + int length = source.length(); + int offset = 0; + while (offset < length) { + // Find next escape sequence + int escPosition = -1; + int escIndex = -1; + for (int i = 0; i < escape.length; i++) { + int index; + if (indexes[i] < offset) { + index = stringSource.indexOf(escape[i][0], offset); + indexes[i] = index; + } else { + index = indexes[i]; + } + + if (index >= 0 && (index < escPosition || escPosition < 0)) { + escPosition = index; + escIndex = i; + } + } + + // If escape secuence is found + if (escPosition >= 0) { + // replace chars before escape sequence + result.append(stringSource, offset, escPosition); + + // Replace sequence + result.append(escape[escIndex][1]); + offset = escPosition + escape[escIndex][0].length(); + } else { + // Put other string to result sequence + result.append(stringSource, offset, length); + offset = length; + } + } + } + + return result; + } +} diff --git a/src/ru/perm/kefir/bbcode/EscapeXmlProcessorFactory.java b/src/ru/perm/kefir/bbcode/EscapeXmlProcessorFactory.java @@ -0,0 +1,59 @@ +package ru.perm.kefir.bbcode; + +/** + * The class for creating the escape xml special symbols processor. It's processor change: + * <p/> + * &amp; to &amp;amp; + * &apos; to &amp;apos; + * &lt; to &amp;lt; + * &gt; to &amp;gt; + * &quot; to &amp;quot; + * + * @author Kefir + */ +public final class EscapeXmlProcessorFactory implements TextProcessorFactory { + /** + * The default XML escape symbols + */ + private static final String[][] DEFAULT_ESCAPE_XML = { + {"&", "&amp;"}, + {"'", "&apos;"}, + {">", "&gt;"}, + {"<", "&lt;"}, + {"\"", "&quot;"} + }; + + /** + * Instance of processor. + */ + private static final TextProcessor processor = new EscapeProcessor(DEFAULT_ESCAPE_XML); + + /** + * Instance of factory + */ + private static final TextProcessorFactory instance = new EscapeXmlProcessorFactory(); + + /** + * Private constructor. Because this class is singleton. + */ + private EscapeXmlProcessorFactory() { + } + + /** + * Return instance of this class. + * + * @return instance of escape xml processor factory + */ + public static TextProcessorFactory getInstance() { + return instance; + } + + /** + * Create the new XML escape symbols processor. + * + * @see ru.perm.kefir.bbcode.TextProcessorFactory#create() + */ + public TextProcessor create() { + return processor; + } +} diff --git a/src/ru/perm/kefir/bbcode/IntSet.java b/src/ru/perm/kefir/bbcode/IntSet.java @@ -0,0 +1,94 @@ +package ru.perm.kefir.bbcode; + +/** + * Best performance set of primitive type int + * + * @author Vitaliy Samolovskih aka Kefir + */ +class IntSet { + private static final int TABLE_SIZE = 256; + private static final int MASK = 255; + private static final int INITIAL_CAPACITY = 16; + + private final int[][] table = new int[TABLE_SIZE][]; // Init null values by default + private final int lengths[] = new int[TABLE_SIZE]; // Init 0 values by default + + IntSet() { + } + + public void add(int value) { + int rowIndex = rowIndex(value); + + int[] row = table[rowIndex]; + if (row == null) { + row = new int[INITIAL_CAPACITY]; + table[rowIndex] = row; + row[0] = value; + lengths[rowIndex]++; + } else { + int length = lengths[rowIndex]; + + // Enlarge array if necessary + if (length >= row.length) { + int newLength = 2 * row.length; + int[] copyRow = new int[newLength]; + System.arraycopy(row, 0, copyRow, 0, row.length); + row = copyRow; + table[rowIndex] = copyRow; + } + + int index = binarySearch(row, length, value); + + // If can't find value + if (index < 0) { + // Insert value + int temp = value; + for (int i = -index - 1; i < length; i++) { + int temp1 = row[i]; + row[i] = temp; + temp = temp1; + } + row[length] = temp; + lengths[rowIndex]++; + } + } + } + + /** + * Realisation of binary search algorithm. It is in JDK 1.6.0 but for + * JDK 1.5.0 compatibility I added it there. + * + * @param array array of integers values ordered by ascending + * @param toIndex top break of array + * @param key searched value + * @return value index or -(index of position) + * @see java.util.Arrays#binarySearch(int[], int, int, int) + */ + private static int binarySearch(int[] array, int toIndex, int key) { + int low = 0; + int high = toIndex - 1; + + while (low <= high) { + int mid = (low + high) >>> 1; + int midVal = array[mid]; + + if (midVal < key) + low = mid + 1; + else if (midVal > key) + high = mid - 1; + else + return mid; // key found + } + return -(low + 1); // key not found. + } + + public boolean contains(int value) { + int rowIndex = rowIndex(value); + int length = lengths[rowIndex]; + return length > 0 && binarySearch(table[rowIndex], length, value) >= 0; + } + + private int rowIndex(int value) { + return value & MASK; + } +} diff --git a/src/ru/perm/kefir/bbcode/NamedElement.java b/src/ru/perm/kefir/bbcode/NamedElement.java @@ -0,0 +1,39 @@ +package ru.perm.kefir.bbcode; + +/** + * Named element are variable and text PatternElement or TemplateElement + */ +public class NamedElement { + /** + * Variable name + */ + private final String name; + + /** + * Create named element + * + * @param name name of element + */ + public NamedElement(String name) { + this.name = name; + } + + /** + * Get element name + * + * @return element name + */ + public String getName() { + return name; + } + + /** + * Add attribute with name of this element name and value <code>value</code> to <code>context</code>. + * + * @param context context + * @param value variable value + */ + protected void setAttribute(Context context, CharSequence value) { + context.setAttribute(name, value); + } +} diff --git a/src/ru/perm/kefir/bbcode/NamedValue.java b/src/ru/perm/kefir/bbcode/NamedValue.java @@ -0,0 +1,26 @@ +package ru.perm.kefir.bbcode; + +/** + * Named value to build target text + */ +public class NamedValue extends NamedElement implements TemplateElement { + public NamedValue(String name) { + super(name); + } + + /** + * Добавляет элемент в новую строку + * + * @param context контекст + */ + public CharSequence generate(Context context) { + Object attribute = context.getAttribute(getName()); + if (attribute == null) { + return "null"; + } else if (attribute instanceof CharSequence) { + return (CharSequence) attribute; + } else { + return attribute.toString(); + } + } +} diff --git a/src/ru/perm/kefir/bbcode/Pattern.java b/src/ru/perm/kefir/bbcode/Pattern.java @@ -0,0 +1,74 @@ +package ru.perm.kefir.bbcode; + +import java.util.Collections; +import java.util.List; + +/** + * Represents the pattern + * + * @author Vitaliy Samolovskih aka Kefir + */ +public class Pattern { + /** + * Pattern elements + */ + private final List<? extends PatternElement> elements; + + // Performance optimization + private final PatternElement firstElement; + + /** + * Construct pattern. + * + * @param elements pattern elements + */ + public Pattern(List<? extends PatternElement> elements) { + this.elements = Collections.unmodifiableList(elements); + + // Performance optimization + if (!this.elements.isEmpty()) { + firstElement = this.elements.get(0); + } else { + throw new IllegalArgumentException("Parameter \"elements\" can't be empty."); + } + } + + /** + * Указывает на то что следующая последовательность вполне может оказаться данным тэгом + * + * @param source источник + * @return true - если следующие несколько символов совпадают с первой константой в коде + * false - означает, что это точно не тот код + */ + public boolean suspicious(Source source) { + return firstElement.isNextIn(source); + } + + /** + * Parse context with this pattern + * + * @param context current context + * @return true if next subsequence is valid to this pattern, + * false others + */ + public boolean parse(Context context) { + boolean flag = true; + int start = context.getSource().getOffset(); + int patternSize = elements.size(); + for (int i = 0; i < patternSize && flag; i++) { + PatternElement current = elements.get(i); + PatternElement next; + if (i < patternSize - 1) { + next = elements.get(i + 1); + } else { + next = context.getTerminator(); + } + flag = context.getSource().hasNext() && current.parse(context, next); + } + + if (!flag) { + context.getSource().setOffset(start); + } + return flag; + } +} diff --git a/src/ru/perm/kefir/bbcode/PatternElement.java b/src/ru/perm/kefir/bbcode/PatternElement.java @@ -0,0 +1,35 @@ +package ru.perm.kefir.bbcode; + +/** + * Pattern element for parse part of bbcode + * + * @author Kefir + */ +public interface PatternElement { + /** + * Parse element + * + * @param context context + * @param terminator teminator to stop text process + * @return true - subsequence is valid to this pattern + * false - not valid + */ + public boolean parse(Context context, PatternElement terminator); + + /** + * Check next subsequence + * + * @param source source text + * @return true pattern sequence equals with next subsequence + * false not equals + */ + public boolean isNextIn(Source source); + + /** + * Find constant + * + * @param source text source + * @return constant offset + */ + public int findIn(Source source); +} diff --git a/src/ru/perm/kefir/bbcode/Scope.java b/src/ru/perm/kefir/bbcode/Scope.java @@ -0,0 +1,145 @@ +package ru.perm.kefir.bbcode; + +import java.util.*; + +/** + * bb-code scope. Required for tables, for example. + * Scope contains code set for parsing text. + * + * @author Vitaliy Samolovskih aka Kefir + */ +public class Scope { + /** + * Default name for root scope. If ROOT scope not defined in configuration + * then all codes add to defqult ROOT scope. + */ + public static final String ROOT = "ROOT"; + + /** + * Name of scope + */ + private final String name; + + /** + * Parent scope for inherit codes + */ + private Scope parent = null; + + /** + * Code set for current scope without parent scope codes + */ + private Set<? extends AbstractCode> scopeCodes = null; + + /** + * Mark that not parsiable text must not append to result + */ + private boolean ignoreText = false; + + /** + * Code of scope include the parent codes + */ + private List<AbstractCode> cachedCodes = null; + + /** + * Mark that this scope is initialized + */ + private boolean initialized = false; + + /** + * Create scope + * + * @param name name of scope + */ + public Scope(String name) { + this.name = name; + } + + /** + * Get scope name + * + * @return scope name + */ + public String getName() { + return name; + } + + /** + * Set parent scope + * + * @param parent parent scope. All parent scope code added to scope codes. + */ + public void setParent(Scope parent) { + this.parent = parent; + cacheCodes(); + } + + /** + * Add codes to scope + * + * @param codes code set + */ + public void setScopeCodes(Set<? extends AbstractCode> codes) { + this.scopeCodes = codes; + cacheCodes(); + initialized = true; + } + + /** + * Return all scope codes include parent codes. + * + * @return list of codes in priority order. + */ + public List<AbstractCode> getCodes() { + if (!initialized) { + throw new IllegalStateException("Scope is not initialized."); + } + return cachedCodes; + } + + /** + * Cache scope codes. Join scope codes with parent scope codes. + */ + private void cacheCodes() { + List<AbstractCode> list = new ArrayList<AbstractCode>(); + if (parent != null) { + list.addAll(parent.getCodes()); + } + if (scopeCodes != null) { + list.addAll(scopeCodes); + } + Collections.sort(list, + new Comparator<AbstractCode>() { + public int compare(AbstractCode code1, AbstractCode code2) { + return code2.compareTo(code1); + } + } + ); + cachedCodes = Collections.unmodifiableList(list); + } + + /** + * By default it is false. + * + * @return true if not parsiable text mustn't append to result. + * false if not parsiable text append to result as is. + */ + public boolean isIgnoreText() { + return ignoreText; + } + + /** + * Set flag marked that not parsiable text mustn't append to result. By default it is false. + * + * @param ignoreText flag value + */ + public void setIgnoreText(boolean ignoreText) { + this.ignoreText = ignoreText; + } + + /** + * @return true if scope was initialised, false otherwise + */ + public boolean isInitialized() { + return initialized; + } +} diff --git a/src/ru/perm/kefir/bbcode/Source.java b/src/ru/perm/kefir/bbcode/Source.java @@ -0,0 +1,208 @@ +package ru.perm.kefir.bbcode; + +/** + * Класс источник для парсинга BB-кодов + * + * @author Kefir + */ +public class Source { + private static final int BUFF_SIZE = 4096; + + /** + * Текст ждля парсинга + */ + private final CharSequence text; + private final int textLength; + + /** + * Смещение + */ + private int offset = 0; + private char currentChar; + + /** + * Создает класс источник + * + * @param text исходный текст + */ + public Source(CharSequence text) { + this.text = text; + textLength = text.length(); + updateCurrentChar(); + } + + public int find(String value) { + if (text instanceof String) { + return ((String) text).indexOf(value, offset); + } else if (text instanceof StringBuilder) { + return ((StringBuilder) text).indexOf(value, offset); + } else if (text instanceof StringBuffer) { + return ((StringBuffer) text).indexOf(value, offset); + } else { + int inCharSequence = findInCharSequence(text.subSequence(offset, textLength), value); + if (inCharSequence >= 0) { + return offset + inCharSequence; + } else { + return -1; + } + } + } + + /** + * Find value in character sequence + * + * @param sequence character sequence + * @param value searched value + * @return index of value in sequence + */ + private int findInCharSequence(CharSequence sequence, String value) { + if (value.length()==0) { + throw new IllegalArgumentException("Argument value can't be empty."); + } + + final int seqLength = sequence.length(); + final int valLength = value.length(); + + if (seqLength < valLength) { + return -1; + } + + int index; + int size; + + int nextSize = Math.max(BUFF_SIZE, valLength); + do { + size = nextSize; + if (size > seqLength) { + size = seqLength; + } + + index = sequence.subSequence(0, size).toString().indexOf(value); + nextSize = 2 * size; + } while (index <= 0 && size < seqLength); + + return index; + } + + /** + * Возвращает следующий симвойл и увеличивает смещение + * + * @return символ + */ + public char next() { + char c = current(); + offset++; + updateCurrentChar(); + return c; + } + + public char current() { + return currentChar; + } + + /** + * Возвращает текущее смещение + * + * @return смещение от начала + */ + public int getOffset() { + return offset; + } + + /** + * Increament offset + */ + public void incOffset() { + offset++; + updateCurrentChar(); + } + + private void updateCurrentChar() { + if (offset < textLength) { + currentChar = text.charAt(offset); + } + } + + /** + * Увеличивает смещение + * + * @param increment на сколько нужно увеличить смещение + */ + public void incOffset(int increment) { + offset += increment; + updateCurrentChar(); + } + + /** + * Устанавливает смещение + * + * @param offset смещение + */ + public void setOffset(int offset) { + this.offset = offset; + updateCurrentChar(); + } + + /** + * Есть ли еще что-то в строке + * + * @return true - если есть + * false если достигнут конец строки + */ + public boolean hasNext() { + return offset < textLength; + } + + /** + * Есть ли еще count символов в строке + * + * @param count количчество символов которое должно остаться в строке + * @return true - если есть + * false если достигнут конец строки + */ + public boolean hasNext(int count) { + return (textLength - offset) >= count; + } + + /** + * Return length of sorce text + * + * @return length of source text + */ + public int getLength() { + return textLength; + } + + /** + * Получает строку от текущего смещения до значения <code>end</code> + * + * @param end последний индекс + * @return подстрока + */ + public CharSequence sub(int end) { + return text.subSequence(getOffset(), end); + } + + /** + * Get String from offset to offset+valueLength + * + * @param count length of extracted string + * @return string + */ + public CharSequence subTo(int count) { + return sub(getOffset() + count); + } + + /** + * Get String from offset to end + * + * @return string + */ + public CharSequence subToEnd() { + return sub(textLength); + } + + public String toString() { + return "ru.perm.kefir.bbcode.Source,length:" + String.valueOf(textLength); + } +} diff --git a/src/ru/perm/kefir/bbcode/Template.java b/src/ru/perm/kefir/bbcode/Template.java @@ -0,0 +1,44 @@ +package ru.perm.kefir.bbcode; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +/** + * Code template + * + * @author Vitaliy Samolovskih aka Kefir + */ +public class Template { + /** + * Empty template + */ + @SuppressWarnings({"unchecked"}) + public static final Template EMPTY = new Template(Collections.EMPTY_LIST); + + /** + * Template elemnts + */ + private final List<? extends TemplateElement> elements; + + /** + * Create neq template with elements. + * + * @param elements template elements. + */ + public Template(List<? extends TemplateElement> elements) { + this.elements = Collections.unmodifiableList(elements); + } + + /** + * Append to result string processed text. + * + * @param context current context. + * @throws IOException if can't append. + */ + public void generate(Context context) throws IOException { + for (TemplateElement element : elements) { + context.getTarget().append(element.generate(context)); + } + } +} diff --git a/src/ru/perm/kefir/bbcode/TemplateElement.java b/src/ru/perm/kefir/bbcode/TemplateElement.java @@ -0,0 +1,14 @@ +package ru.perm.kefir.bbcode; + +/** + * The element of template to build target text + */ +public interface TemplateElement { + /** + * Append template element to source of context + * + * @param context контекст + * @return builded text + */ + public CharSequence generate(Context context); +} diff --git a/src/ru/perm/kefir/bbcode/Text.java b/src/ru/perm/kefir/bbcode/Text.java @@ -0,0 +1,91 @@ +package ru.perm.kefir.bbcode; + +import java.io.IOException; + +/** + * Класс текста, который подлежит парсингу + * + * @author Kefir + */ +public class Text extends NamedElement implements PatternElement { + /** + * Scope defina the codeset for parsing this text + */ + private final Scope scope; + + /** + * Mark that variables getted in element context will be put into parent context + */ + private final boolean transparent; + + /** + * Создает именованный элемент + * + * @param name имя переменной + * @param transparent mark that scope variable must be accessible from parent context + */ + public Text(String name, boolean transparent) { + super(name); + scope = null; + this.transparent = transparent; + } + + public Text(String name, Scope scope, boolean transparent) { + super(name); + this.scope = scope; + this.transparent = transparent; + } + + /** + * Парсит элемент + * + * @param context контекст + * @return true - если удалось распарсить константу + * false - если не удалось + */ + public boolean parse(Context context, PatternElement terminator) { + Context child = new Context(context); + StringBuilder target = new StringBuilder(); + child.setTarget(target); + if (scope != null) { + child.setScope(scope); + } + child.setTerminator(terminator); + try { + child.parse(); + } catch (IOException e) { + // Never because StringBuilder don't throw IOException + } + if(transparent){ + child.mergeWithParent(); + } + setAttribute(context, target); + return true; + } + + /** + * Определяет, что дальше в разбираемой строке находится нужная последовательность + * + * @param source source text + * @return true если следующие символы в строке совпадают с pattern + * false если не совпадают или строка кончилась + */ + public boolean isNextIn(Source source) { + return false; + } + + /** + * Find this element + * + * @param source text source + * @return start offset + */ + public int findIn(Source source) { + return -1; + } + + @Override + public String toString() { + return "text:" + getName(); + } +} diff --git a/src/ru/perm/kefir/bbcode/TextProcessor.java b/src/ru/perm/kefir/bbcode/TextProcessor.java @@ -0,0 +1,44 @@ +package ru.perm.kefir.bbcode; + +/** + * The interface of text processors + * + * @author Kefir + */ +public interface TextProcessor { + /** + * Process the text. + * <p/> + * ATTENTION!!! Do not use java.nio.CharBuffer. + * CharBuffer has invalid realization of subSequence methos from interface java.lang.CharSequence + * since 1.6.0_10 version of JRE. http://bugs.sun.com/view_bug.do?bug_id=6795561 + * + * @param source the sourcetext + * @return the result of text processing + */ + public CharSequence process(CharSequence source); + + /** + * Process the text + * + * @param source the sourcetext + * @return the result of text processing + */ + public String process(String source); + + /** + * Process the text + * + * @param source the sourcetext + * @return the result of text processing + */ + public StringBuilder process(StringBuilder source); + + /** + * Process the text + * + * @param source the sourcetext + * @return the result of text processing + */ + public StringBuffer process(StringBuffer source); +} diff --git a/src/ru/perm/kefir/bbcode/TextProcessorAdapter.java b/src/ru/perm/kefir/bbcode/TextProcessorAdapter.java @@ -0,0 +1,53 @@ +package ru.perm.kefir.bbcode; + +/** + * Text Processor adapter implement methods for String, StringBuffer, StringBuilder + * + * @author Vitaliy Samolovskih aka Kefir + */ +public abstract class TextProcessorAdapter implements TextProcessor { + /** + * Process the text + * + * @param source the sourcetext + * @return the result of text processing + */ + public String process(String source) { + CharSequence result = process((CharSequence) source); + if (result instanceof String) { + return (String) result; + } else { + return result.toString(); + } + } + + /** + * Process the text + * + * @param source the sourcetext + * @return the result of text processing + */ + public StringBuilder process(StringBuilder source) { + CharSequence result = process((CharSequence) source); + if (result instanceof StringBuilder) { + return (StringBuilder) result; + } else { + return new StringBuilder(result); + } + } + + /** + * Process the text + * + * @param source the sourcetext + * @return the result of text processing + */ + public StringBuffer process(StringBuffer source) { + CharSequence result = process((CharSequence) source); + if (result instanceof StringBuffer) { + return (StringBuffer) result; + } else { + return new StringBuffer(result); + } + } +} diff --git a/src/ru/perm/kefir/bbcode/TextProcessorChain.java b/src/ru/perm/kefir/bbcode/TextProcessorChain.java @@ -0,0 +1,35 @@ +package ru.perm.kefir.bbcode; + +import java.util.Collections; +import java.util.List; + +/** + * Chain of text processors wich process text serially + * + * @author Kefir + */ +public class TextProcessorChain extends TextProcessorAdapter { + /** + * List of processors + */ + private final List<? extends TextProcessor> processors; + + public TextProcessorChain(List<? extends TextProcessor> processors) { + this.processors = Collections.unmodifiableList(processors); + } + + /** + * Process the text + * + * @param source the sourcetext + * @return the result of text processing + * @see TextProcessor#process(CharSequence) + */ + public CharSequence process(CharSequence source) { + CharSequence target = source; + for (TextProcessor processor : processors) { + target = processor.process(target); + } + return target; + } +} diff --git a/src/ru/perm/kefir/bbcode/TextProcessorFactory.java b/src/ru/perm/kefir/bbcode/TextProcessorFactory.java @@ -0,0 +1,16 @@ +package ru.perm.kefir.bbcode; + +/** + * The TextProcessor factory interface + * + * @author Kefir + */ +public interface TextProcessorFactory { + /** + * Create the TextProcessor instance + * + * @return instance of TextProcessor interface + * @throws TextProcessorFactoryException when factory can't create the TextProcessor instance + */ + public TextProcessor create(); +} diff --git a/src/ru/perm/kefir/bbcode/TextProcessorFactoryException.java b/src/ru/perm/kefir/bbcode/TextProcessorFactoryException.java @@ -0,0 +1,24 @@ +package ru.perm.kefir.bbcode; + +/** + * Exception if TextProcessorFactory can't create the TextProcessor instance + * + * @author Kefir + */ +public class TextProcessorFactoryException extends RuntimeException { + public TextProcessorFactoryException() { + super(); + } + + public TextProcessorFactoryException(String message) { + super(message); + } + + public TextProcessorFactoryException(String message, Throwable cause) { + super(message, cause); + } + + public TextProcessorFactoryException(Throwable cause) { + super(cause); + } +} diff --git a/src/ru/perm/kefir/bbcode/Util.java b/src/ru/perm/kefir/bbcode/Util.java @@ -0,0 +1,38 @@ +package ru.perm.kefir.bbcode; + +import java.io.InputStream; +import java.util.UUID; + +/** + * Util class + * + * @author Vitaliy Samolovskih aka Kefir + */ +final class Util { + private Util() { + } + + /** + * Open the resource stream for named resource. + * Stream must be closed by user after usage. + * + * @param resourceName resource name + * @return input stream + */ + static InputStream openResourceStream(String resourceName) { + InputStream stream = null; + ClassLoader classLoader = Util.class.getClassLoader(); + if (classLoader != null) { + stream = classLoader.getResourceAsStream(resourceName); + } + + if (stream == null) { + stream = ClassLoader.getSystemResourceAsStream(resourceName); + } + return stream; + } + + static String generateRandomName() { + return UUID.randomUUID().toString(); + } +} diff --git a/src/ru/perm/kefir/bbcode/Variable.java b/src/ru/perm/kefir/bbcode/Variable.java @@ -0,0 +1,115 @@ +package ru.perm.kefir.bbcode; + +import java.util.regex.Matcher; + +/** + * Класс переменной + * + * @author Kefir + */ +public class Variable extends NamedElement implements PatternElement { + private final java.util.regex.Pattern regex; + + /** + * Создает именованную переменную + * + * @param name название переменной + */ + public Variable(String name) { + super(name); + regex = null; + } + + /** + * Create named variable + * + * @param name variable name + * @param regex regular expression pattern + */ + public Variable(String name, java.util.regex.Pattern regex) { + super(name); + this.regex = regex; + } + + /** + * Парсит элемент + * + * @param context контекст + * @return true - если удалось распарсить константу + * false - если не удалось + */ + public boolean parse(Context context, PatternElement terminator) { + int end; + if (terminator != null) { + end = terminator.findIn(context.getSource()); + } else { + end = context.getSource().getLength(); + } + + if (end < 0) { + return false; + } + + Source source = context.getSource(); + CharSequence value = source.sub(end); + + // If define regex, then find this regex in value + if (regex != null) { + Matcher matcher = regex.matcher(value); + if (matcher.lookingAt()) { + int lend = matcher.end(); + end = source.getOffset() + lend; + value = value.subSequence(0, lend); + } else { + return false; + } + } + + // Test this variable already defined and equals with this in this code scope + Object attr = context.getLocalAttribute(getName()); + if (attr == null || attr.equals(value)) { + if(attr==null){ + setAttribute(context, value); + } + source.setOffset(end); + return true; + } else { + return false; + } + } + + /** + * Определяет, что дальше в разбираемой строке находится нужная последовательность + * + * @param source source text + * @return true если следующие символы в строке совпадают с pattern + * false если не совпадают или строка кончилась + */ + public boolean isNextIn(Source source) { + return regex != null && regex.matcher(source.subToEnd()).lookingAt(); + } + + /** + * Find this element + * + * @param source text source + * @return start offset + */ + public int findIn(Source source) { + if (regex != null) { + Matcher matcher = regex.matcher(source.subToEnd()); + if (matcher.find()) { + return matcher.start(); + } else { + return -1; + } + } else { + return -1; + } + } + + @Override + public String toString() { + return "variable:" + getName(); + } +} diff --git a/src/ru/perm/kefir/bbcode/default.xml b/src/ru/perm/kefir/bbcode/default.xml @@ -0,0 +1,236 @@ +<?xml version="1.0" encoding="utf-8"?> +<configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns="http://kefir-bb.sourceforge.net/schemas" + xsi:schemaLocation="http://kefir-bb.sourceforge.net/schemas http://kefir-bb.sourceforge.net/schemas/kefir-bb-0.5.xsd"> + <!-- XML escape symbols --> + <scope name="escapeXml"> + <code priority="100"> + <pattern>&amp;</pattern> + <template>&amp;amp;</template> + </code> + <code priority="100"> + <pattern>&apos;</pattern> + <template>&amp;apos;</template> + </code> + <code priority="100"> + <pattern>&lt;</pattern> + <template>&amp;lt;</template> + </code> + <code priority="100"> + <pattern>&gt;</pattern> + <template>&amp;gt;</template> + </code> + <code priority="100"> + <pattern>&quot;</pattern> + <template>&amp;quot;</template> + </code> + </scope> + + <!-- Scope for escaping bb spec chars --> + <scope name="escapeBb" parent="escapeXml"> + <!-- + Escape bb-code symbols + double slash to slash + slash + square bracket to square bracket + --> + <code name="slash" priority="10"> + <pattern>\\</pattern> + <template>\</template> + </code> + <code name="left_square_bracket" priority="9"> + <pattern>\[</pattern> + <template>[</template> + </code> + <code name="right_square_bracket" priority="9"> + <pattern>\]</pattern> + <template>]</template> + </code> + <coderef name="comment"/> + </scope> + + <!-- Comment --> + <code name="comment"> + <pattern>[*<var parse="false"/>*]</pattern> + <template/> + </code> + + <!-- Escape basic HTML char sequences --> + <scope name="basic" parent="escapeBb"> + <!-- line feed characters --> + <code name="br1" priority="3"> + <pattern>&#x0A;&#x0D;</pattern> + <template>&lt;br/&gt;</template> + </code> + <code name="br2" priority="2"> + <pattern>&#x0D;&#x0A;</pattern> + <template>&lt;br/&gt;</template> + </code> + <code name="br3" priority="1"> + <pattern>&#x0A;</pattern> + <template>&lt;br/&gt;</template> + </code> + <code name="br4" priority="0"> + <pattern>&#x0D;</pattern> + <template>&lt;br/&gt;</template> + </code> + + <!-- Special html symbols --> + <code name="symbol"> + <pattern>[symbol=<var scope="escapeXml"/>/]</pattern> + <template>&amp;<var/>;</template> + </code> + + <!-- angle quotes --> + <code name="aquote"> + <pattern>[aquote]<var inherit="true"/>[/aquote]</pattern> + <template>&amp;laquo;<var/>&amp;raquo;</template> + </code> + </scope> + + <!-- Root scope. This scope uses when processor started work and by default, if not set other scope --> + <scope name="ROOT" parent="basic"> + <!-- Formatting --> + <coderef name="bold"/> + <coderef name="u"/> + <coderef name="s"/> + <coderef name="i"/> + <coderef name="color"/> + <coderef name="size"/> + + <!-- Quotes --> + <coderef name="code"/> + <coderef name="quote"/> + + <!-- Images --> + <coderef name="img1"/> + <coderef name="img2"/> + + <!-- links --> + <coderef name="url1"/> + <coderef name="url2"/> + <coderef name="url3"/> + <coderef name="url4"/> + <coderef name="url5"/> + <coderef name="url6"/> + + <!-- Table --> + <coderef name="table"/> + </scope> + + <!-- Simple formatting --> + <code name="bold"> + <pattern>[b]<var inherit="true"/>[/b]</pattern> + <template>&lt;span style=&quot;font-weight:bold;&quot;&gt;<var/>&lt;/span&gt;</template> + </code> + <code name="u"> + <pattern>[u]<var inherit="true"/>[/u]</pattern> + <template>&lt;span style=&quot;text-decoration:underline;&quot;&gt;<var/>&lt;/span&gt;</template> + </code> + <code name="s"> + <pattern>[s]<var inherit="true"/>[/s]</pattern> + <template>&lt;span style=&quot;text-decoration:line-through;&quot;&gt;<var/>&lt;/span&gt;</template> + </code> + <code name="i"> + <pattern>[i]<var inherit="true"/>[/i]</pattern> + <template>&lt;span style=&quot;font-style:italic;&quot;&gt;<var/>&lt;/span&gt;</template> + </code> + + <!-- Font color --> + <code name="color"> + <pattern>[color=<var name="color" scope="escapeXml"/>]<var name="text" inherit="true"/>[/color]</pattern> + <template>&lt;span style=&quot;color:<var name="color"/>;&quot;&gt;<var name="text"/>&lt;/span&gt;</template> + </code> + + <!-- Font size --> + <code name="size"> + <pattern>[size=<var name="size" scope="escapeXml"/>]<var name="text" inherit="true"/>[/size]</pattern> + <template>&lt;span style=&quot;font-size:<var name="size"/>;&quot;&gt;<var name="text"/>&lt;/span&gt;</template> + </code> + + <!-- Insert image --> + <code name="img1" priority="2"> + <pattern>[img]<var name="protocol" regex="((ht|f)tps?:|\.{1,2})?"/>/<var name="addr" scope="escapeXml"/>[/img]</pattern> + <template>&lt;img src=&quot;<var name="protocol"/>/<var name="addr"/>&quot;/&gt;</template> + </code> + <code name="img2" priority="1"> + <pattern>[img]<var name="addr" scope="escapeXml"/>[/img]</pattern> + <template>&lt;img src=&quot;http://<var name="addr"/>&quot;/&gt;</template> + </code> + + <!-- Links. http, https, malto protocols --> + <scope name="url" parent="basic"> + <coderef name="bold"/> + <coderef name="u"/> + <coderef name="s"/> + <coderef name="i"/> + <coderef name="color"/> + <coderef name="size"/> + + <coderef name="img1"/> + <coderef name="img2"/> + </scope> + + <!-- HTTP --> + <code name="url1" priority="2"> + <pattern>[url=<var name="protocol" regex="((ht|f)tps?:|\.{1,2})?"/>/<var name="url" scope="escapeXml"/>]<var name="text" scope="url"/>[/url]</pattern> + <template>&lt;a href=&quot;<var name="protocol"/>/<var name="url"/>&quot;&gt;<var name="text"/>&lt;/a&gt;</template> + </code> + <code name="url2" priority="2"> + <pattern>[url]<var name="protocol" regex="((ht|f)tps?:|\.{1,2})?"/>/<var name="url" scope="escapeXml"/>[/url]</pattern> + <template>&lt;a href=&quot;<var name="protocol"/>/<var name="url"/>&quot;&gt;<var name="protocol"/>/<var name="url"/>&lt;/a&gt;</template> + </code> + <code name="url3" priority="1"> + <pattern>[url=<var name="url" scope="escapeXml"/>]<var name="text" scope="url"/>[/url]</pattern> + <template>&lt;a href=&quot;http://<var name="url"/>&quot;&gt;<var name="text"/>&lt;/a&gt;</template> + </code> + <code name="url4" priority="1"> + <pattern>[url]<var name="url" scope="escapeXml"/>[/url]</pattern> + <template>&lt;a href=&quot;http://<var name="url"/>&quot;&gt;<var name="url"/>&lt;/a&gt;</template> + </code> + + <!-- MAILTO --> + <code name="url5" priority="2"> + <pattern>[url=mailto:<var name="url" scope="escapeXml"/>]<var name="text" scope="url"/>[/url]</pattern> + <template>&lt;a href=&quot;mailto:<var name="url"/>&quot;&gt;<var name="text"/>&lt;/a&gt;</template> + </code> + <code name="url6" priority="2"> + <pattern>[url]mailto:<var name="url" scope="escapeXml"/>[/url]</pattern> + <template>&lt;a href=&quot;mailto:<var name="url"/>&quot;&gt;mailto:<var name="url"/>&lt;/a&gt;</template> + </code> + + <!-- Qote block --> + <code name="quote"> + <pattern>[quote]<var inherit="true"/>[/quote]</pattern> + <template>&lt;em&gt;<var/>&lt;/em&gt;</template> + </code> + + <!-- Quote code block --> + <code name="code"> + <pattern>[code]<var scope="basic"/>[/code]</pattern> + <template>&lt;pre&gt;<var/>&lt;/pre&gt;</template> + </code> + + <!-- Simple table --> + <code name="table"> + <pattern>[table]<var scope="tableScope"/>[/table]</pattern> + <template>&lt;table&gt;<var/>&lt;/table&gt;</template> + </code> + <scope name="tableScope" ignoreText="true"> + <code name="tr"> + <pattern>[tr]<var scope="trScope"/>[/tr]</pattern> + <template>&lt;tr&gt;<var/>&lt;/tr&gt;</template> + </code> + <coderef name="comment"/> + </scope> + <scope name="trScope" ignoreText="true"> + <code name="th"> + <pattern>[th]<var/>[/th]</pattern> + <template>&lt;th&gt;<var/>&lt;/th&gt;</template> + </code> + <code name="td"> + <pattern>[td]<var/>[/td]</pattern> + <template>&lt;td&gt;<var/>&lt;/td&gt;</template> + </code> + <coderef name="comment"/> + </scope> +</configuration>+ \ No newline at end of file