package water.util; import java.io.IOException; import java.net.URLEncoder; import java.util.*; import java.util.Map.Entry; import water.Key; import water.H2O; /** * A replaceable string that allows very easy and simple replacements. * * %placeholder is normally inserted * * %$placeholder is inserted in URL encoding for UTF-8 charset. This should be * used for all hrefs. * */ public class RString { // A placeholder information with replcement group and start and end labels. private static class Placeholder { LabelledStringList.Label start; LabelledStringList.Label end; RString group; // Creates new placeholder private Placeholder(LabelledStringList.Label start, LabelledStringList.Label end) { this.start = start; this.end = end; this.group = null; } // Creates new placeholder for replacement group private Placeholder(LabelledStringList.Label start, LabelledStringList.Label end, String from) { this.start = start; this.end = end; this.group = new RString(from, this); } } // Placeholders MMHashMap<String, Placeholder> _placeholders; // Parts of the final string (replacements and originals together). LabelledStringList _parts; // Parent placeholder if the RString is a replacement group. Placeholder _parent; // Passes only valid placeholder name characters static private boolean isIdentChar(char x) { return (x == '$') || ((x >= 'a') && (x <= 'z')) || ((x >= 'A') && (x <= 'Z')) || ((x >= '0') && (x <= '9')) || (x == '_'); } // Creates a string that is itself a replacement group. private RString(final String from, Placeholder parent) { this(from); _parent = parent; } // Creates the RString from given normal string. The placeholders must begin // with % sign and if the placeholder name is followed by { }, the placeholder // is treated as a replacement group. Replacement groups cannot be tested. // Only letters, numbers and underscore can form a placeholder name. In the // constructor the string is parsed into parts and placeholders so that all // replacements in the future are very quick (hashmap lookup in fact). public RString(final String from) { _parts = new LabelledStringList(); _placeholders = new MMHashMap<>(); LabelledStringList.Label cur = _parts.begin(); int start = 0; int end = 0; while( true ) { start = from.indexOf("%", end); if( start == -1 ) { cur.insertAndAdvance(from.substring(end, from.length())); break; } ++start; if( start == from.length() ) { throw new ArrayIndexOutOfBoundsException(); } if( from.charAt(start) == '%' ) { cur.insertAndAdvance(from.substring(end, start)); end = start + 1; } else { cur.insertAndAdvance(from.substring(end, start - 1)); end = start; while( (end < from.length()) && (isIdentChar(from.charAt(end))) ) { ++end; } String pname = from.substring(start, end); if( (end == from.length()) || (from.charAt(end) != '{') ) { // it is a normal placeholder _placeholders.put2(pname, new Placeholder(cur.clone(), cur.clone())); } else { // it is another RString start = end + 1; end = from.indexOf("}", end); if( end == -1 ) { throw new ArrayIndexOutOfBoundsException("Missing } after replacement group"); } _placeholders.put2(pname, new Placeholder(cur.clone(), cur.clone(), from.substring(start, end))); ++end; } } } } // Returns the string with all replaced material. @Override public String toString() { return _parts.toString(); } // Removes all replacements from the string (keeps the placeholders so that // they can be used again. private void clear() { //for( Placeholder p : _placeholders.values() ) { // p.start.removeTill(p.end); //} throw H2O.unimpl(); } public void replace(String what, Key key) { replace(what, key.user_allowed() ? key.toString() : "<code>"+key.toString()+"</code>"); } // Replaces the given placeholder with an object. On a single placeholder, // multiple replaces can be called in which case they are appended one after // another in order. public void replace(String what, Object with) { if (what.charAt(0)=='$') throw new RuntimeException("$ is now control char that denotes URL encoding!"); for (Placeholder p : _placeholders.get(what)) p.end.insertAndAdvance(with.toString()); ArrayList<Placeholder> ar = _placeholders.get("$"+what); if( ar == null ) return; for (Placeholder p : ar) try { p.end.insertAndAdvance(URLEncoder.encode(with.toString(),"UTF-8")); } catch (IOException e) { p.end.insertAndAdvance(e.toString()); } } // Returns a replacement group of the given name and clears it so that it // can be filled again. private RString restartGroup(String what) { List<Placeholder> all = _placeholders.get(what); assert all.size() == 1; Placeholder result = all.get(0); if( result.group == null ) { throw new NoSuchElementException("Element " + what + " is not a group."); } result.group.clear(); return result.group; } // If the RString itself is a replacement group, adds its contents to the // placeholder. private void append() { if( _parent == null ) { throw new UnsupportedOperationException("Cannot append if no parent is specified."); } _parent.end.insertAndAdvance(toString()); } private static class MMHashMap<K,V> extends HashMap<K,ArrayList<V>> { void put2( K key, V val ) { ArrayList<V> ar = get(key); if( ar==null ) put(key,ar = new ArrayList<>()); ar.add(val); } } } /** * List that has labels to it (something like copyable iterators) and some very * basic functionality for it. * * Since it is not a private class only the things we require are filled in. The * labels do not expect or deal with improper use, so make sure you know what * you are doing when using directly this class. */ class LabelledStringList { // Inner item of the list, single linked private static class Item { String value; Item next; Item(String value, Item next) { this.value = value; this.next = next; } } // Label to the list, which acts as a restricted form of an iterator. Notably // a label can be used to add items in the middle of the list and also to // delete all items in between two labels. class Label { // element before the label Item _prev; // Creates the label from given inner list item so that the label points // right after it. If null, label points at the very beginnig of the list. Label(Item prev) { _prev = prev; } // Creates a new copy of the label that points to the same place @Override protected Label clone() { return new Label(_prev); } // Inserts new string after the label private void insert(String value) { if( _prev == null ) { _begin = new Item(value, _begin); } else { _prev.next = new Item(value, _prev.next); } ++_noOfElements; _length += value.length(); } // Inserts new string after the label and then advances the label. Thus in // theory inserting before the label. void insertAndAdvance(String value) { insert(value); if( _prev == null ) { _prev = _begin; } else { _prev = _prev.next; } } // Removes the element after the label. private void remove() throws NoSuchElementException { if( _prev == null ) { if( _begin == null ) { throw new NoSuchElementException(); } _length -= _begin.value.length(); _begin = _begin.next; } else { if( _prev.next == null ) { throw new NoSuchElementException(); } _length -= _prev.next.value.length(); _prev.next = _prev.next.next; } --_noOfElements; } // Removes all elements between the label and the other label. The other // label must come after the first label, otherwise everything after the // label will be deleted. private void removeTill(Label other) { if( _prev == null ) { if( other._prev == null ) { return; } while( ((_begin != null) && (_begin.next != other._prev.next)) ) { _length -= _begin.value.length(); _begin = _begin.next; --_noOfElements; } } else { if( other._prev == null ) { clear(); _prev = null; } else { Item end = other._prev.next; while( (_prev.next != null) && (_prev.next != end) ) { remove(); } } } other._prev = _prev; } } // first item private Item _begin; // length in characters of the total stored string private int _length; // number of String elemets stored private int _noOfElements; // Creates an empty string list LabelledStringList() { _length = 0; _noOfElements = 0; } // Returns a label to the first item Label begin() { return new Label(null); } // Returns the number of elements stored in the list private int length() { return _noOfElements; } // Clears all elements in the list (all labels should be cleared by the // user when calling this method). private void clear() { _begin = null; _length = 0; _noOfElements = 0; } // Concatenates all parts of the string and returns them as single string @Override public String toString() { StringBuilder s = new StringBuilder(_length); Item i = _begin; while( i != null ) { s.append(i.value); i = i.next; } return s.toString(); } }