DynamicViewCustomizer


package mcl.reports;

/*
 * The latest version is available from https://github.com/jesse-gallagher/Domino-One-Offs/blob/master/mcl/reports/DynamicViewCustomizer.java
 */


import java.io.*;
import java.nio.charset.Charset;
import java.util.Date;
import java.util.List;
import java.util.Vector;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.el.ValueBinding;

import lotus.domino.*;

import com.ibm.commons.util.SystemCache;
import com.ibm.xsp.FacesExceptionEx;
import com.ibm.xsp.extlib.builder.ControlBuilder.IControl;
import com.ibm.xsp.extlib.component.dynamicview.UIDynamicViewPanel;
import com.ibm.xsp.extlib.component.dynamicview.ViewColumnConverter;
import com.ibm.xsp.extlib.component.dynamicview.ViewDesign;
import com.ibm.xsp.extlib.component.dynamicview.DominoDynamicColumnBuilder.DominoViewCustomizer;
import com.ibm.xsp.extlib.component.dynamicview.ViewDesign.ColumnDef;
import com.ibm.xsp.extlib.component.dynamicview.ViewDesign.DefaultColumnDef;
import com.ibm.xsp.extlib.component.dynamicview.ViewDesign.DefaultViewDef;
import com.ibm.xsp.extlib.component.dynamicview.ViewDesign.ViewDef;
import com.ibm.xsp.extlib.component.dynamicview.ViewDesign.ViewFactory;
import com.ibm.xsp.extlib.util.ExtLibUtil;
import com.ibm.xsp.model.domino.wrapped.DominoViewEntry;
import com.raidomatic.xml.XMLDocument;
import com.raidomatic.xml.XMLNode;


public class DynamicViewCustomizer extends DominoViewCustomizer implements Serializable {
	private static final long serialVersionUID = -5126984721484501732L;

	private String panelId = "";

	@Override
	public ViewFactory getViewFactory() {
		return new DynamicViewFactory();
	}

	public static class DynamicViewFactory implements ViewFactory, Serializable {
		private static final long serialVersionUID = 123034173761337005L;
		private SystemCache views = new SystemCache("View Definition", 16, "xsp.extlib.viewdefsize");

		private void writeObject(ObjectOutputStream out) throws IOException {
			this.views = null;
			out.defaultWriteObject();
		}
		private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
			in.defaultReadObject();
			this.views = new SystemCache("View Definition", 16, "xsp.extlib.viewdefsize");
		}

		public ViewDef getViewDef(View view) {
			if(view == null) {
				return null;
			}
			try {
				String viewKey = ViewDesign.getViewKey(view);
				DefaultViewDef viewDef = (DefaultViewDef)this.views.get(viewKey);
				if(viewDef == null) {
					// Read the view
					viewDef = new DefaultViewDef();
					if(view.isHierarchical()) { viewDef.flags |= DefaultViewDef.FLAG_HIERARCHICAL; }
					if(view.isCategorized()) { viewDef.flags |= DefaultViewDef.FLAG_CATEGORIZED; }

					viewDef.columns.addAll(this.getViewColumnInformation(view));
				}
				return viewDef;
			} catch(Exception ex) {
				throw new FacesExceptionEx(ex, "Error while accessing view {0}", view.toString());
			}
		}

		@SuppressWarnings("unchecked")
		private List<ColumnDef> getViewColumnInformation(View view) throws Exception {
			Database database = view.getParent();

			/* Generate the DXL */
			Document viewDoc = database.getDocumentByUNID(view.getUniversalID());
			String dxl = viewDoc.generateXML();
			InputStream is = new ByteArrayInputStream(dxl.getBytes(Charset.forName("UTF-8")));
			XMLDocument dxlDoc = new XMLDocument();
			dxlDoc.loadInputStream(is);
			viewDoc.recycle();

			/*
			 * Fetch both types of column information since some properties are
			 * much easier to deal with via the standard API
			 */
			List<ViewColumn> viewColumns = view.getColumns();
			List<XMLNode> dxlColumns = dxlDoc.selectNodes("//column");

			// Figure out if we're going to extend the last column
			boolean extendLastColumn = dxlDoc.selectSingleNode("//view").getAttribute("extendlastcolumn").equals("true");

			Document contextDoc = database.createDocument();
			List<ColumnDef> columns = new Vector<ColumnDef>();
			String activeColorColumn = "";
			for(int i = 0; i < dxlColumns.size(); i++) {
				XMLNode columnNode = dxlColumns.get(i);
				ViewColumn viewColumn = viewColumns.get(i);

				ExtendedColumnDef column = new ExtendedColumnDef();
				column.replicaId = view.getParent().getReplicaID();

				if(columnNode.getAttribute("hidden").equals("true")) {
					column.flags |= DefaultColumnDef.FLAG_HIDDEN;
				} else {
					// Check to see if it's hidden by a hide-when formula
					XMLNode hideWhen = columnNode.selectSingleNode("code[@event='hidewhen']");
					if(hideWhen != null) {
						if(hideWhen.getAttribute("enabled") == null || !hideWhen.getAttribute("enabled").equals("false")) {
							String hideWhenFormula = hideWhen.getText();
							if(hideWhenFormula.length() > 0) {
								List<Object> evalResult = ExtLibUtil.getCurrentSession().evaluate(hideWhenFormula, contextDoc);
								if(evalResult.size() > 0 && evalResult.get(0) instanceof Double && (Double)evalResult.get(0) == 1) {
									column.flags |= DefaultColumnDef.FLAG_HIDDEN;
								}
							}
						}
					}
				}

				column.name = columnNode.getAttribute("itemname");

				if(columnNode.getAttribute("showascolor").equals("true")) {
					activeColorColumn = column.name;
				}
				column.colorColumn = activeColorColumn;

				// Get the header information
				XMLNode header = columnNode.selectSingleNode("columnheader");
				column.title = columnNode.selectSingleNode("columnheader").getAttribute("title");
				if(header.getAttribute("align").equals("center")) {
					column.flags |= DefaultColumnDef.FLAG_HALIGNCENTER;
				} else if(header.getAttribute("align").equals("right")) {
					column.flags |= DefaultColumnDef.FLAG_HALIGNRIGHT;
				}

				column.width = new Float(columnNode.getAttribute("width")).intValue();
				column.actualWidth = Double.parseDouble(columnNode.getAttribute("width"));

				if(columnNode.getAttribute("responsesonly").equals(true)) {
					column.flags |= DefaultColumnDef.FLAG_RESPONSE;
				}
				if(columnNode.getAttribute("categorized").equals("true")) {
					column.flags |= DefaultColumnDef.FLAG_CATEGORIZED;
				}
				if(columnNode.getAttribute("sort").length() > 0) {
					column.flags |= DefaultColumnDef.FLAG_SORTED;
				}
				if(columnNode.getAttribute("resort").equals("ascending") || columnNode.getAttribute("resort").equals("both")) {
					column.flags |= DefaultColumnDef.FLAG_RESORTASC;
				}
				if(columnNode.getAttribute("resort").equals("descending") || columnNode.getAttribute("resort").equals("both")) {
					column.flags |= DefaultColumnDef.FLAG_RESORTDESC;
				}
				if(columnNode.getAttribute("align").equals("center")) {
					column.flags |= DefaultColumnDef.FLAG_ALIGNCENTER;
				} else if(columnNode.getAttribute("align").equals("right")) {
					column.flags |= DefaultColumnDef.FLAG_ALIGNRIGHT;
				}
				if(columnNode.getAttribute("showaslinks").equals("true")) {
					column.flags |= DefaultColumnDef.FLAG_LINK | DefaultColumnDef.FLAG_ONCLICK | DefaultColumnDef.FLAG_CHECKBOX;
				}

				column.numberFmt = viewColumn.getNumberFormat();
				column.numberDigits = viewColumn.getNumberDigits();
				column.numberAttrib = viewColumn.getNumberAttrib();
				if(viewColumn.isNumberAttribParens()) {
					column.flags |= DefaultColumnDef.FLAG_ATTRIBPARENS;
				}
				if(viewColumn.isNumberAttribPercent()) {
					column.flags |= DefaultColumnDef.FLAG_ATTRIBPERCENT;
				}
				if(viewColumn.isNumberAttribPunctuated()) {
					column.flags |= DefaultColumnDef.FLAG_ATTRIBPUNC;
				}
				column.timeDateFmt = viewColumn.getTimeDateFmt();
				column.dateFmt = viewColumn.getDateFmt();
				column.timeFmt = viewColumn.getTimeFmt();
				column.timeZoneFmt = viewColumn.getTimeZoneFmt();
				column.listSep = viewColumn.getListSep();

				column.fontFace = viewColumn.getFontFace();
				column.fontStyle = viewColumn.getFontStyle();
				column.fontPointSize = viewColumn.getFontPointSize();
				column.fontColor = viewColumn.getFontColor();

				column.headerFontFace = viewColumn.getHeaderFontFace();
				column.headerFontStyle = viewColumn.getHeaderFontStyle();
				column.headerFontPointSize = viewColumn.getHeaderFontPointSize();
				column.headerFontColor = viewColumn.getHeaderFontColor();

				if(columnNode.getAttribute("showasicons").equals("true")) {
					column.flags |= DefaultColumnDef.FLAG_ICON;
				}
				if(columnNode.getAttribute("twisties").equals("true")) {
					column.flags |= DefaultColumnDef.FLAG_INDENTRESP;
				}

				// Find any twistie image
				XMLNode twistieImage = columnNode.selectSingleNode("twistieimage/imageref");
				if(twistieImage != null) {
					if(twistieImage.getAttribute("database").equals("0000000000000000")) {
						column.twistieReplicaId = database.getReplicaID();
					} else {
						column.twistieReplicaId = twistieImage.getAttribute("database");
					}

					// Make sure that the referenced database is available on
					// the current server
					boolean setTwistie = true;
					if(!column.twistieReplicaId.equalsIgnoreCase(database.getReplicaID())) {
						Database twistieDB = ExtLibUtil.getCurrentSession().getDatabase("", "");
						twistieDB.openByReplicaID("", column.twistieReplicaId);
						if(!twistieDB.isOpen()) {
							setTwistie = false;
						}
						twistieDB.recycle();
					}
					if(setTwistie) {
						column.twistieImage = twistieImage.getAttribute("name");
					}

				}

				// Support extending the column width to the full window.
				// In the client, "extend last column" takes priority
				if(extendLastColumn && i == dxlColumns.size() - 1) {
					column.extendColumn = true;
				} else if(!extendLastColumn && columnNode.getAttribute("extwindowwidth").equals("true")) {
					column.extendColumn = true;
				}

				columns.add(column);

				viewColumn.recycle();
			}
			contextDoc.recycle();

			database.recycle();

			return columns;
		}

		public static class ExtendedColumnDef extends DefaultColumnDef implements Serializable {
			private static final long serialVersionUID = 5158008403553374867L;

			public String colorColumn;
			public String twistieImage = "";
			public String twistieReplicaId = "";
			public boolean extendColumn = false;
			public String replicaId = "";
			public double actualWidth;

			public String fontFace = "";
			public int fontPointSize = 0;
			public int fontStyle = 0;
			public int fontColor = 0;

			public String headerFontFace = "";
			public int headerFontPointSize = 0;
			public int headerFontStyle = 0;
			public int headerFontColor = 0;
		}
	}
	@Override
	public IControl createColumn(FacesContext context, UIDynamicViewPanel panel, int index, ColumnDef colDef) {
		this.panelId = panel.getId();
		return super.createColumn(context, panel, index, colDef);
	}

	@Override
	public void afterCreateColumn(FacesContext context, int index, ColumnDef colDef, IControl column) {
		UIDynamicViewPanel panel = (UIDynamicViewPanel)ExtLibUtil.getComponentFor(context.getViewRoot(), panelId);

		// Patch in a converter to handle special text and icon columns
		UIDynamicViewPanel.DynamicColumn col = (UIDynamicViewPanel.DynamicColumn)column.getComponent();
		if(colDef.isIcon()) {
			// For icons, override the default behavior so it can handle
			// string-based ones
			col.setValueBinding("iconSrc", null);
			col.setDisplayAs("");
			col.setConverter(new IconColumnConverter(null, colDef, panel));
		} else {
			col.setConverter(new ExtendedViewColumnConverter(null, colDef, panel));
		}

		// Apply a general style class to indicate that it's not just some
		// normal view panel column
		// Many style attributes will be class-based both for flexibility and
		// because headers can't have style applied directly
		String styleClass = " notesViewColumn";
		String headerStyleClass = "";

		// Add an extra class for category columns
		if(colDef.isCategorized()) {
			styleClass += " notesViewCategory";
		}

		// We'll handle escaping the HTML manually, to support
		// [<b>Notes-style</b>] pass-through-HTML and icon columns
		col.setContentType("html");

		// Deal with any twistie images and color columns
		if(colDef instanceof DynamicViewFactory.ExtendedColumnDef) {
			DynamicViewFactory.ExtendedColumnDef extColDef = (DynamicViewFactory.ExtendedColumnDef)colDef;

			if(extColDef.twistieImage.length() > 0) {
				// Assume that it's a multi-image well for now
				col.setCollapsedImage("/.ibmxspres/domino/__" + extColDef.twistieReplicaId + ".nsf/" + extColDef.twistieImage.replaceAll("\\\\", "/") + "?Open&ImgIndex=2");
				col.setExpandedImage("/.ibmxspres/domino/__" + extColDef.twistieReplicaId + ".nsf/" + extColDef.twistieImage.replaceAll("\\\\", "/") + "?Open&ImgIndex=1");
			}

			// The style applies to the contents of the column as well as the
			// column itself, which messes with icon columns
			// For now, don't apply it at all to those columns
			String style = "";
			if(!extColDef.extendColumn) {
				style = "max-width: " + (extColDef.actualWidth * extColDef.fontPointSize * 1.3) + "px; min-width: " + (extColDef.actualWidth * extColDef.fontPointSize * 1.3) + "px";
			} else {
				style = "width: 100%";
			}

			// Check for left or right alignment
			switch(extColDef.getAlignment()) {
			case ViewColumn.ALIGN_CENTER:
				styleClass += " notesViewAlignCenter";
				break;
			case ViewColumn.ALIGN_RIGHT:
				styleClass += " notesViewAlignRight";
				break;
			}

			// Add font information
			styleClass += this.fontStyleToStyleClass(extColDef.fontStyle);
			headerStyleClass += this.fontStyleToStyleClass(extColDef.headerFontStyle);
			style += "; color: " + this.notesColorToCSS(extColDef.fontColor);

			if(extColDef.colorColumn.length() > 0) {
				String styleFormula = "#{javascript:'" + style.replace("'", "\\'") + ";' + " + this.getClass().getName() + ".colorColumnToStyle(" + panel.getVar() + ".getColumnValue('"
				+ extColDef.colorColumn + "'))}";
				ValueBinding styleBinding = context.getApplication().createValueBinding(styleFormula);
				col.setValueBinding("style", styleBinding);
			} else {
				col.setStyle(style);
			}

		}
		col.setStyleClass((col.getStyleClass() == null ? "" : col.getStyleClass()) + styleClass);
		col.setHeaderClass((col.getHeaderClass() == null ? "" : col.getHeaderClass()) + headerStyleClass);
	}

	public static class ExtendedViewColumnConverter extends ViewColumnConverter {
		private ColumnDef colDef;
		private String panelId;

		// For loading the state
		public ExtendedViewColumnConverter() {}

		public ExtendedViewColumnConverter(ViewDef viewDef, ColumnDef colDef, UIDynamicViewPanel panel) {
			super(viewDef, colDef);
			this.colDef = colDef;
			this.panelId = panel.getId();
		}

		@Override
		public String getAsString(FacesContext context, UIComponent component, Object value) {
			UIDynamicViewPanel panel = (UIDynamicViewPanel)ExtLibUtil.getComponentFor(context.getViewRoot(), panelId);

			// First, apply any column-color info needed
			DominoViewEntry entry = this.resolveViewEntry(context, panel.getVar());
			try {
				if(value instanceof DateTime) {
					return this.getValueDateTimeAsString(context, component, ((DateTime)value).toJavaDate());
				}
				if(value instanceof Date) {
					return this.getValueDateTimeAsString(context, component, (Date)value);
				}
				if(value instanceof Number) {
					return this.getValueNumberAsString(context, component, (Number)value);
				}
			} catch(NotesException ex) {}

			String stringValue = value.toString();

			try {
				stringValue = specialTextDecode(stringValue, entry);
			} catch(NotesException ne) {}

			// Process the entry as Notes-style pass-through-HTML
			if(!entry.isCategory()) {
				stringValue = this.handlePassThroughHTML(stringValue);
			}

			// Add in some text for empty categories
			if(entry.isCategory() && stringValue.length() == 0) {
				stringValue = "(Not Categorized)";
			}

			// Include a &nbsp; to avoid weird styling problems when the content
			// itself is empty or not visible
			return stringValue;
		}

		private String handlePassThroughHTML(String cellData) {
			if(cellData.contains("[<") && cellData.contains(">]")) {
				String[] cellChunks = cellData.split("\\[\\<", -2);
				cellData = "";
				for(String chunk : cellChunks) {
					if(chunk.contains(">]")) {
						String[] smallChunks = chunk.split(">]", -2);
						cellData += "<" + smallChunks[0] + ">" + xmlEncode(smallChunks[1]);
					} else {
						cellData += xmlEncode(chunk);
					}
				}
			} else {
				cellData = xmlEncode(cellData);
			}
			return cellData;
		}

		private DominoViewEntry resolveViewEntry(FacesContext context, String var) {
			return (DominoViewEntry)context.getApplication().getVariableResolver().resolveVariable(context, var);
		}

		@Override
		public Object saveState(FacesContext context) {
			Object[] superState = (Object[])super.saveState(context);
			Object[] state = new Object[3];
			state[0] = superState;
			state[1] = this.colDef;
			state[2] = this.panelId;
			return state;
		}

		@Override
		public void restoreState(FacesContext context, Object value) {
			Object[] state = (Object[])value;
			super.restoreState(context, state[0]);
			this.colDef = (ColumnDef)state[1];
			this.panelId = (String)state[2];
		}
	}

	public static class IconColumnConverter extends ViewColumnConverter {
		private ColumnDef colDef;

		// For loading the state
		public IconColumnConverter() {}

		public IconColumnConverter(ViewDef viewDef, ColumnDef colDef, UIDynamicViewPanel panel) {
			super(viewDef, colDef);
			this.colDef = colDef;
		}

		@SuppressWarnings("unchecked")
		@Override
		public String getAsString(FacesContext context, UIComponent component, Object value) {
			List<Object> listValue;
			if(value instanceof List) {
				listValue = (List<Object>)value;
			} else {
				listValue = new Vector<Object>();
				listValue.add(value);
			}
			StringBuilder result = new StringBuilder();

			result.append("<span style='white-space: nowrap'>");
			for(Object node : listValue) {
				// Handle a zero-value icon specially
				if(node instanceof Double && ((Double)node == 0 || (Double)node == 999)) {
					result.append("<img class='notesViewIconCustom notesViewIconBlank' src='/icons/ecblank.gif' />");
				} else if(node instanceof Double) {
					result.append("<img class='notesViewIconStandard' src='/icons/vwicn");
					Double num = (Double)node;
					if(num < 10) {
						result.append("00");
					} else if(num < 100) {
						result.append("0");
					}
					result.append(num.intValue());
					result.append(".gif' />");
				} else {
					if(String.valueOf(value).length() > 0 && !String.valueOf(value).equals("null")) {
						DynamicViewFactory.ExtendedColumnDef col = (DynamicViewFactory.ExtendedColumnDef)this.colDef;
						try {
							result.append("<img class='notesViewIconCustom' src='");
							result.append("/__" + col.replicaId + ".nsf/" + java.net.URLEncoder.encode(String.valueOf(node), "UTF-8"));
							result.append("' />");
						} catch(Exception e) {}
					}
				}
			}
			result.append("</span>");

			return result.toString();
		}

		@Override
		public Object saveState(FacesContext context) {
			Object[] superState = (Object[])super.saveState(context);
			Object[] state = new Object[2];
			state[0] = superState;
			state[1] = this.colDef;
			return state;
		}

		@Override
		public void restoreState(FacesContext context, Object value) {
			Object[] state = (Object[])value;
			super.restoreState(context, state[0]);
			this.colDef = (ColumnDef)state[1];
		}

	}

	@SuppressWarnings("unchecked")
	public static String colorColumnToStyle(Object colorValuesObj) {
		String cellStyle = "";
		if(colorValuesObj instanceof List) {
			List<Double> colorValues = (List<Double>)colorValuesObj;
			if(colorValues.size() > 3) {
				// Then we have a background color
				if(colorValues.get(0) != -1) {
					// Then the background is not pass-through
					cellStyle = "background-color: rgb(" + colorValues.get(0).intValue() + ", " + colorValues.get(1).intValue() + ", " + colorValues.get(2).intValue() + ");";
				} else {
					cellStyle = "";
				}
				if(colorValues.get(3) != -1) {
					// Then main color is not pass-through
					cellStyle += "color: rgb(" + colorValues.get(3).intValue() + ", " + colorValues.get(4).intValue() + ", " + colorValues.get(5).intValue() + ");";
				}
			} else {
				// Then it's just a text color
				if(colorValues.get(0) != -1) {
					cellStyle += "color: rgb(" + colorValues.get(0).intValue() + ", " + colorValues.get(1).intValue() + ", " + colorValues.get(2).intValue() + ");";
				}
			}
		}
		return cellStyle;
	}

	private String fontStyleToStyleClass(int fontStyle) {
		StringBuilder result = new StringBuilder();

		if((fontStyle & ViewColumn.FONT_PLAIN) != 0) {
			result.append(" notesViewPlain");
		}
		if((fontStyle & ViewColumn.FONT_BOLD) != 0) {
			result.append(" notesViewBold");
		}
		if((fontStyle & ViewColumn.FONT_UNDERLINE) != 0) {
			result.append(" notesViewUnderline");
		}
		if((fontStyle & ViewColumn.FONT_STRIKETHROUGH) != 0) {
			result.append(" notesViewStrikethrough");
		}
		if((fontStyle & ViewColumn.FONT_ITALIC) != 0) {
			result.append(" notesViewItalic");
		}

		return result.toString();
	}

	private String notesColorToCSS(int notesColor) {
		try {
			Session session = ExtLibUtil.getCurrentSession();
			ColorObject colorObject = session.createColorObject();
			colorObject.setNotesColor(notesColor);

			StringBuilder result = new StringBuilder();
			result.append("rgb(");
			result.append(colorObject.getRed());
			result.append(",");
			result.append(colorObject.getGreen());
			result.append(",");
			result.append(colorObject.getBlue());
			result.append(")");

			return result.toString();
		} catch(NotesException ne) {
			return "";
		}
	}

	public static String strLeft(String input, String delimiter) {
		return input.substring(0, input.indexOf(delimiter));
	}
	public static String strRight(String input, String delimiter) {
		return input.substring(input.indexOf(delimiter) + delimiter.length());
	}
	public static String strLeftBack(String input, String delimiter) {
		return input.substring(0, input.lastIndexOf(delimiter));
	}
	public static String strLeftBack(String input, int chars) {
		return input.substring(0, input.length() - chars);
	}
	public static String strRightBack(String input, String delimiter) {
		return input.substring(input.lastIndexOf(delimiter) + delimiter.length());
	}
	public static String strRightBack(String input, int chars) {
		return input.substring(input.length() - chars);
	}

	public static String xmlEncode(String text) {
		StringBuilder result = new StringBuilder();

		for(int i = 0; i < text.length(); i++) {
			char currentChar = text.charAt(i);
			if(!((currentChar >= 'a' && currentChar <= 'z') || (currentChar >= 'A' && currentChar <= 'Z') || (currentChar >= '0' && currentChar <= '9'))) {
				result.append("&#" + (int)currentChar + ";");
			} else {
				result.append(currentChar);
			}
		}

		return result.toString();
	}

	public static String specialTextDecode(String specialText, ViewEntry viewEntry) throws NotesException {
		String result = specialText;

		String specialStart = "";
		String specialEnd = "�";

		// First, find the start and end of the special text
		int start_pos = result.indexOf(specialStart);
		int end_pos = result.indexOf(specialEnd);

		int loopStopper = 1;
		while(start_pos > -1 && end_pos > start_pos && loopStopper < 100) {
			loopStopper++;

			String working = result.substring(start_pos+1, end_pos);
			String midResult = "";
			String[] choices;
			int offset, length, parameterCount;

			switch(working.charAt(0)) {
			case 'C':
				// @DocChildren
				parameterCount = Integer.parseInt(working.substring(1, 2));
				switch(parameterCount) {
				case 0:
					midResult = viewEntry.getChildCount() + "";
					break;
				case 1:
					midResult = strRight(working, "=").replaceAll("%", viewEntry.getChildCount() + "");
					break;
				case 2:
					// For convenience, I'll break the string into each option, even if I only use one
					choices = new String[] { "", "" };

					// I can cheat a bit on the first one to find the length
					offset = 0;
					length = Integer.parseInt(strLeft(strRight(working, ";"), "="));
					choices[0] = working.substring(working.indexOf("=", offset)+1, working.indexOf("=", offset)+1+length);

					offset = working.indexOf("=", offset) + 1 + length;
					choices[1] = working.substring(working.indexOf("=", offset)+1, working.indexOf("=", offset)+1+length);

					if(viewEntry.getChildCount() == 0) {
						midResult = choices[0].replaceAll("%", "0");
					} else {
						midResult = choices[1].replaceAll("%", viewEntry.getChildCount() + "");
					}

					break;
				case 3:
					// For convenience, I'll break the string into each option, even if I only use one
					choices = new String[] { "", "", "" };

					// I can cheat a bit on the first one to find the length
					offset = 0;
					length = Integer.parseInt(strLeft(strRight(working, ";"), "="));
					choices[0] = working.substring(working.indexOf("=", offset)+1, working.indexOf("=", offset)+1+length);

					offset = working.indexOf("=", offset) + 2 + length;
					length = Integer.parseInt(working.substring(offset, working.indexOf("=", offset)));
					choices[1] = working.substring(working.indexOf("=", offset)+1, working.indexOf("=", offset)+1+length);

					offset = working.indexOf("=", offset) + 2 + length;
					length = Integer.parseInt(working.substring(offset, working.indexOf("=", offset)));
					choices[2] = working.substring(working.indexOf("=", offset)+1, working.indexOf("=", offset)+1+length);

					if(viewEntry.getChildCount() == 0) {
						midResult = choices[0].replaceAll("%", "0");
					} else if(viewEntry.getChildCount() == 1) {
						midResult = choices[1].replaceAll("%", "1");
					} else {
						midResult = choices[2].replaceAll("%", viewEntry.getChildCount() + "");
					}

					break;
				}
				break;
			case 'D':
				// @DocDescendants
				parameterCount = Integer.parseInt(working.substring(1, 2));
				switch(parameterCount) {
				case 0:
					midResult = viewEntry.getDescendantCount() + "";
					break;
				case 1:
					midResult = strRight(working, "=").replaceAll("%", viewEntry.getDescendantCount() + "");
					break;
				case 2:
					// For convenience, I'll break the string into each option, even if I only use one
					choices = new String[] { "", "" };

					// I can cheat a bit on the first one to find the length
					offset = 0;
					length = Integer.parseInt(strLeft(strRight(working, ";"), "="));
					choices[0] = working.substring(working.indexOf("=", offset)+1, working.indexOf("=", offset)+1+length);

					offset = working.indexOf("=", offset) + 1 + length;
					choices[1] = working.substring(working.indexOf("=", offset)+1, working.indexOf("=", offset)+1+length);

					if(viewEntry.getDescendantCount() == 0) {
						midResult = choices[0].replaceAll("%", "0");
					} else {
						midResult = choices[1].replaceAll("%", viewEntry.getDescendantCount() + "");
					}

					break;
				case 3:
					// For convenience, I'll break the string into each option, even if I only use one
					choices = new String[] { "", "", "" };

					// I can cheat a bit on the first one to find the length
					offset = 0;
					length = Integer.parseInt(strLeft(strRight(working, ";"), "="));
					choices[0] = working.substring(working.indexOf("=", offset)+1, working.indexOf("=", offset)+1+length);

					offset = working.indexOf("=", offset) + 2 + length;
					length = Integer.parseInt(working.substring(offset, working.indexOf("=", offset)));
					choices[1] = working.substring(working.indexOf("=", offset)+1, working.indexOf("=", offset)+1+length);

					offset = working.indexOf("=", offset) + 2 + length;
					length = Integer.parseInt(working.substring(offset, working.indexOf("=", offset)));
					choices[2] = working.substring(working.indexOf("=", offset)+1, working.indexOf("=", offset)+1+length);

					if(viewEntry.getDescendantCount() == 0) {
						midResult = choices[0].replaceAll("%", "0");
					} else if(viewEntry.getDescendantCount() == 1) {
						midResult = choices[1].replaceAll("%", "1");
					} else {
						midResult = choices[2].replaceAll("%", viewEntry.getDescendantCount() + "");
					}

					break;
				}
				break;
			case 'H':
				// @DocLevel
				midResult = (viewEntry.getIndentLevel()+1) + ""; 
				break;
			case 'A':
				// @DocNumber
				parameterCount = Integer.parseInt(working.substring(1, 2));
				switch(parameterCount) {
				case 0:
					midResult = viewEntry.getPosition('.');
					break;
				case 1:
					String delimiter = strRight(working, "=");
					if(delimiter.length() == 0) {
						midResult = strRightBack(viewEntry.getPosition('.'), ".");
					} else if(delimiter.length() > 1) {
						// Mimic formula's weird behavior for multi-character strings
						midResult = delimiter;
					} else {
						midResult = viewEntry.getPosition(delimiter.charAt(0));
					}
					break;
				}
				break;
			case 'J':
				// @DocParentNumber
				if(viewEntry.getIndentLevel() == 0) {
					midResult = "";
				} else {
					parameterCount = Integer.parseInt(working.substring(1, 2));
					switch(parameterCount) {
					case 0:
						midResult = strLeftBack(viewEntry.getPosition('.'), ".");
						break;
					case 1:
						String delimiter = strRight(working, "=");
						if(delimiter.length() == 0) {
							midResult = strRightBack(strLeftBack(viewEntry.getPosition('.'), "."), ".");
						} else if(delimiter.length() > 1) {
							// Mimic formula's weird behavior for multi-character strings
							midResult = delimiter;
						} else {
							midResult = strLeftBack(viewEntry.getPosition(delimiter.charAt(0)), delimiter);
						}
						break;
					}
				}
				break;
			case 'B':
				// @DocSiblings
				midResult = (viewEntry.getSiblingCount()) + "";
				break;
			case 'I':
				// @IsCategory
				parameterCount = Integer.parseInt(working.substring(1, 2));
				switch(parameterCount) {
				case 0:
					midResult = viewEntry.isCategory() ? "*" : "";
					break;
				case 1:
					midResult = viewEntry.isCategory() ? strRight(working, "=") : "";
					break;
				case 2:
					// For convenience, I'll break the string into each option, even if I only use one
					choices = new String[] { "", "" };
					offset = 0;
					length = Integer.parseInt(strLeft(strRight(working, ";"), "="));
					choices[0] = working.substring(working.indexOf("=", offset)+1, working.indexOf("=", offset)+1+length);

					offset = working.indexOf("=", offset) + 2 + length;
					length = Integer.parseInt(working.substring(offset, working.indexOf("=", offset)));
					choices[1] = working.substring(working.indexOf("=", offset)+1, working.indexOf("=", offset)+1+length);

					midResult = viewEntry.isCategory() ? choices[0] : choices[1];

					break;
				}

				break;
			case 'G':
				// @IsExpandable
				midResult = "";
				break;
			default:
				midResult = working;
			break;
			}

			result = result.replaceAll(specialStart + working + specialEnd, midResult);

			start_pos = result.indexOf(specialStart);
			end_pos = result.indexOf(specialEnd);
		}

		return result;
	}
}
All code submitted to OpenNTF XSnippets, whether submitted as a "Snippet" or in the body of a Comment, is provided under the Apache License Version 2.0. See Terms of Use for full details.
No comments yetLogin first to comment...