001: package org.mmbase.util.functions;
002:
003: import java.util.*;
004: import java.util.regex.*;
005:
006: import org.mmbase.cache.Cache;
007: import org.mmbase.core.event.NodeEvent;
008: import org.mmbase.core.event.NodeEventListener;
009: import org.mmbase.bridge.*;
010: import org.mmbase.bridge.util.*;
011: import org.mmbase.util.transformers.RomanTransformer;
012: import org.mmbase.module.core.*;
013: import org.mmbase.storage.search.*;
014: import org.mmbase.util.logging.Logger;
015: import org.mmbase.util.logging.Logging;
016:
017: /**
018: * The index node functions can be assigned to nodes which are connected by an 'index' relation. An
019: * index relation is an extension of 'posrel', but also has an 'index' and a 'root' field. These are
020: * used to calcaluate the 'number' of the connected nodes. The 'pos' field only serves to fix the order.
021: *
022: * The index field can be empty in which case the node with the lowest pos gets number 1, the
023: * following number 2 and so on. But on any one of the index relations the index can be stated
024: * explicitely. If e.g. the index is specified 'a' then the following node will be 'b'. You can also
025: * arrange for 'i', 'ii', 'iii', 'iv' and so on.
026: *
027: * The root field (a node-type field) specifies to which tree the relations belong. So principaly
028: * the same 'chapter' can exists with several different chapter numbers. Also, it is used to define
029: * where counting must starts, because the complete number of a chapter consists of a chain of
030: * numbers (like 2.3.4.iii).
031: *
032: * If the index is specified to be '-' then that means that it is explicitely empty. That cannot be
033: * '' or otherwise unfilled, because that means 'determin automaticly' already.
034: *
035: *
036: * @author Michiel Meeuwissen
037: * @version $Id: IndexFunction.java,v 1.15 2007/11/25 18:25:49 nklasens Exp $
038: * @since MMBase-1.8
039: */
040: public class IndexFunction extends FunctionProvider {
041:
042: private static final Logger log = Logging
043: .getLoggerInstance(IndexFunction.class);
044:
045: protected static Cache<String, String> indexCache = new Cache<String, String>(
046: 400) {
047: public String getName() {
048: return "IndexNumberCache";
049: }
050:
051: public String getDescription() {
052: return "rootNumber/objectNumber -> Index";
053: }
054:
055: };
056:
057: static {
058: indexCache.putCache();
059:
060: }
061:
062: private static NodeEventListener observer = null;
063:
064: private static synchronized void initObserver() {
065: if (observer == null) {
066: NodeEventListener o = null;
067: try {
068: o = new NodeEventListener() {
069: public void notify(NodeEvent event) {
070: nodeChanged(event.getMachine(), event
071: .getNodeNumber(), event
072: .getBuilderName());
073: }
074:
075: public void nodeChanged(String machine, int number,
076: String builder) {
077: log.info("Received change " + machine + "/"
078: + number + "/" + builder);
079: indexCache.clear(); // this could be done smarter.
080: }
081: };
082: MMObjectBuilder indexRelation = MMBase.getMMBase()
083: .getBuilder("indexrel");
084: indexRelation.addEventListener(o);
085: } catch (Exception e) {
086: log.service("" + e + " retrying later");
087: return;
088: }
089: observer = o;
090: }
091: }
092:
093: /**
094: * Returns the 'successor' of a string. Which means that e.g. after 'zzz' follows 'aaaa'.
095: */
096: public static String successor(String index) {
097: StringBuilder buf = new StringBuilder(index);
098: boolean lowercase = true;
099: for (int i = index.length() - 1; i >= 0; i--) {
100: char c = buf.charAt(i);
101: if (c == '-') {
102: // this special case means 'explicitely empty'.
103: log.debug("Found explicit empty");
104: if (i + 1 < index.length() && buf.charAt(i + 1) == '-') {
105: return "--";
106: }
107: break;
108: } else if (c >= 'a' && c <= 'y') {
109: buf.setCharAt(i, (char) (c + 1));
110: return buf.toString();
111: } else if (c == 'z') {
112: buf.setCharAt(i, 'a');
113: continue;
114: } else if (c >= 'A' && c <= 'Y') {
115: buf.setCharAt(i, (char) (c + 1));
116: return buf.toString();
117: } else if (c == 'Z') {
118: lowercase = false;
119: buf.setCharAt(i, 'A');
120: continue;
121: } else if (c < 128) {
122: buf.setCharAt(i, (char) (c + 1));
123: return buf.toString();
124: } else {
125: buf.setCharAt(i, (char) 65);
126: continue;
127: }
128: }
129:
130: if (lowercase) {
131: buf.insert(0, 'a');
132: } else {
133: buf.insert(0, 'A');
134: }
135: return buf.toString();
136: }
137:
138: /**
139: * Calculates the 'successor' of a roman number, preserving uppercase/lowercase.
140: */
141: protected static String romanSuccessor(String index) {
142: if (index.equals("-"))
143: return "i";
144: if (index.equals("--"))
145: return "--";
146: boolean uppercase = index.length() > 0
147: && Character.isUpperCase(index.charAt(0));
148: String res = RomanTransformer.decimalToRoman(RomanTransformer
149: .romanToDecimal(index) + 1);
150: return uppercase ? res.toUpperCase() : res;
151:
152: }
153:
154: /**
155: * Calculates the 'successor' of an index String. Like '7.4.iii' of which the successor is
156: * '7.4.iv'.
157: *
158: * @param index The string to succeed
159: * @param separator Regular expression to split up the string first (e.g. "\\.")
160: * @param joiner String to rejoin it again (e.g. ".")
161: * @param roman Whether to consider roman numbers
162: */
163: protected static String successor(String index, String separator,
164: String joiner, boolean roman) {
165: if ("-".equals(index))
166: return roman ? "i" : "1";
167: if ("--".equals(index))
168: return "--";
169: String[] split = index.split(separator);
170: //if (split.length == 0) return roman ? "i" : "1";
171:
172: String postfix = split[split.length - 1];
173: if (RomanTransformer.NUMERIC.matcher(postfix).matches()) {
174: postfix = "" + (Integer.parseInt(postfix) + 1);
175: } else {
176: if (!roman
177: || !RomanTransformer.ROMAN.matcher(postfix)
178: .matches()) {
179: postfix = successor(postfix);
180: } else {
181: postfix = romanSuccessor(postfix);
182: }
183: }
184: StringBuilder buf = new StringBuilder();
185: for (int i = 0; i < split.length - 1; i++) {
186: buf.append(split[i]);
187: buf.append(joiner);
188: }
189: buf.append(postfix);
190: return buf.toString();
191: }
192:
193: private static Parameter<Node> ROOT = new Parameter<Node>("root",
194: Node.class, false);
195: private static Parameter<Boolean> ROMAN = new Parameter<Boolean>(
196: "roman", Boolean.class, Boolean.TRUE);
197: private static Parameter<String> SEPARATOR = new Parameter<String>(
198: "separator", String.class, "\\.");
199: private static Parameter<String> JOINER = new Parameter<String>(
200: "joiner", String.class, ".");
201: private static Parameter<String> ROLE = new Parameter<String>(
202: "role", String.class, "index");
203:
204: private static Parameter<?>[] INDEX_ARGS = new Parameter[] {
205: Parameter.CLOUD, ROOT, SEPARATOR, JOINER, ROMAN, ROLE };
206:
207: /**
208: * calculates a key for the cache
209: */
210: private static String getKey(final Node node,
211: final Parameters parameters) {
212: Node root = parameters.get(ROOT);
213: final String role = parameters.get(ROLE);
214: final String join = parameters.get(JOINER);
215: final String separator = parameters.get(SEPARATOR);
216: final boolean roman = parameters.get(ROMAN);
217: return "" + node.getNumber() + "/"
218: + (root == null ? "NULL" : "" + root.getNumber()) + "/"
219: + role + "/" + join + "/" + separator + "/" + roman;
220: }
221:
222: protected static class Stack<C> extends ArrayList<C> {
223: public void push(C o) {
224: add(0, o);
225: }
226:
227: public C pull() {
228: return remove(0);
229: }
230: }
231:
232: protected static NodeFunction<String> index = new NodeFunction<String>(
233: "index", INDEX_ARGS, ReturnType.STRING) {
234: {
235: setDescription("Calculates the index of a node, using the surrounding 'indexrels'");
236: }
237:
238: /**
239: * complete bridge version of {@link #getFunctionValue}
240: */
241: public String getFunctionValue(final Node node,
242: final Parameters parameters) {
243: Node root = parameters.get(ROOT);
244: final String role = parameters.get(ROLE);
245: final String join = parameters.get(JOINER);
246: final String separator = parameters.get(SEPARATOR);
247: final Pattern indexPattern = Pattern.compile("(.+)"
248: + separator + "(.+)");
249: final boolean roman = parameters.get(ROMAN);
250:
251: final String key = getKey(node, parameters);
252:
253: initObserver();
254: String result = indexCache.get(key);
255: if (result != null) {
256: if (log.isDebugEnabled()) {
257: log.debug("Found index '" + result + "' for node "
258: + node.getNumber() + " from cache (key "
259: + key + ")");
260: }
261: return result;
262: }
263: log.debug("Determining index for node " + node.getNumber()
264: + " with role " + role);
265:
266: final NodeManager nm = node.getNodeManager();
267:
268: // now we have to determine the path from node to root.
269:
270: GrowingTreeList tree = new GrowingTreeList(Queries
271: .createNodeQuery(node), 10, nm, role, "source");
272: NodeQuery template = tree.getTemplate();
273: if (root != null) {
274: StepField sf = template.addField(role + ".root");
275: template.setConstraint(template.createConstraint(sf,
276: root));
277: }
278:
279: Stack<Node> stack = new Stack<Node>();
280: TreeIterator it = tree.treeIterator();
281: int depth = it.currentDepth();
282: while (it.hasNext()) {
283: Node n = it.nextNode();
284: if (log.isDebugEnabled()) {
285: log.debug("Considering at " + it.currentDepth()
286: + "/" + depth + " node "
287: + n.getNodeManager().getName() + " "
288: + n.getNumber());
289: }
290: if (it.currentDepth() > depth) {
291: stack.push(n);
292: depth = it.currentDepth();
293: }
294: if (indexCache.contains(getKey(n, parameters))) {
295: if (log.isDebugEnabled()) {
296: log.debug("Index for " + n.getNumber()
297: + " is known already!, breaking");
298: }
299: break;
300: }
301:
302: if (it.currentDepth() < depth) {
303: break;
304: }
305: //if (root == null) root = n.getNodeValue(role + ".root");
306: if (root != null && n.getNumber() == root.getNumber())
307: break;
308: }
309:
310: if (stack.isEmpty()) {
311: log
312: .debug("Stack is empty, no root found, returning ''");
313: indexCache.put(key, "");
314: return "";
315: }
316:
317: if (log.isDebugEnabled()) {
318: log.debug("Now constructing index-number with "
319: + stack.size() + " nodes on stack");
320: }
321: Node n = stack.pull(); // this is root, or at least _its_ index is known
322: StringBuilder buf;
323: if (!n.equals(node)) {
324: buf = new StringBuilder(n.getFunctionValue("index",
325: parameters).toString());
326: } else {
327: buf = new StringBuilder();
328: }
329: String j = buf.length() == 0 ? "" : join;
330: OUTER: while (!stack.isEmpty()) {
331: Node search = stack.pull();
332: NodeQuery q = Queries.createRelatedNodesQuery(n, nm,
333: role, "destination");
334: StepField sf = q.addField(role + ".pos");
335: q.addSortOrder(sf, SortOrder.ORDER_ASCENDING);
336: q.addField(role + ".index");
337: if (log.isDebugEnabled()) {
338: log.debug("Executing " + q.toSql() + " to search "
339: + search.getNumber());
340: }
341: String index = null;
342: NodeIterator ni = q.getCloud().getList(q)
343: .nodeIterator();
344: boolean doRoman = roman;
345: while (ni.hasNext()) {
346: Node clusterFound = ni.nextNode();
347: Node found = clusterFound.getNodeValue(q
348: .getNodeStep().getAlias());
349: String i = clusterFound.getStringValue(role
350: + ".index");
351: if (i == null || i.equals(""))
352: i = index;
353: if (i == null)
354: i = "1";
355: log.debug("Found index " + i);
356: Matcher matcher = indexPattern.matcher(i);
357: if (matcher.matches()) {
358: buf = new StringBuilder(matcher.group(1));
359: i = matcher.group(2);
360: log.debug("matched " + indexPattern + " --> "
361: + i);
362: }
363: doRoman = doRoman
364: && RomanTransformer.ROMAN.matcher(i)
365: .matches();
366:
367: boolean explicitEmpty = "-".equals(i)
368: || "--".equals(i);
369: if (found.getNumber() == search.getNumber()) {
370: log.debug("found sibling");
371: // found!
372: if (!explicitEmpty) {
373: buf.append(j).append(i);
374: } else {
375: buf.setLength(0);
376: }
377: j = join;
378: n = found;
379: continue OUTER;
380: }
381: index = successor(i, separator, join, doRoman);
382:
383: String hapKey = getKey(found, parameters);
384: String value = explicitEmpty ? "" : buf.toString()
385: + j + i;
386: log.debug("Caching " + key + "->" + value);
387: // can as well cache this one too.
388: indexCache.put(hapKey, value);
389: }
390: // not found
391: buf.append(j).append("???");
392: break;
393: }
394: String r = buf.toString();
395: log.debug("Found '" + r + "' for " + key);
396: indexCache.put(key, r);
397: return r;
398: }
399: };
400: {
401: addFunction(index);
402: }
403:
404: public static void main(String argv[]) {
405:
406: CloudContext cc = ContextProvider.getDefaultCloudContext();
407: Cloud cloud = cc.getCloud("mmbase", "class", null);
408: Node node = cloud.getNode(argv[0]);
409: Node root = null;
410: if (argv.length > 1)
411: root = cloud.getNode(argv[1]);
412: Parameters params = index.createParameters();
413: params.set(ROOT, root);
414: params.set(ROMAN, Boolean.TRUE);
415: System.out.println("" + index.getFunctionValue(node, params));
416:
417: }
418:
419: }
|