/* * RHQ Management Platform * Copyright (C) 2005-2011 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package org.rhq.core.domain.util; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.rhq.core.domain.resource.composite.DisambiguationReport; import org.rhq.core.domain.resource.composite.DisambiguationReport.Resource; import org.rhq.core.domain.resource.composite.DisambiguationReport.ResourceType; /** * This class can be used to produce a "pretty" textual representation of a {@link DisambiguationReport}. * It supports a simple templating mechanism to configure the representation's format. * <p> * In the {@link DisambiguationReport}, a resource (either the resource being disambiguated or one of its parents) * is represented by an instance of {@link org.rhq.core.domain.resource.composite.DisambiguationReport.Resource}. The template uses this instance and its * properties: * <ul> * <li>id - the resource id * <li>name - the name of the resource * <li>type - the type of the resource * <ul> * <li>name - the name of the resource type * <li>plugin - the name of the plugin defining the resource type * <li>singleton - a boolean whether the resource type is a singleton type or not * </ul> * </li> * </ul> * <p> * The template has the following format:<br/> * <code>...text...%([..text...])?<identifier>([...text...])?...text...</code> * <p> * It is best explained by an example. The {@link #DEFAULT_SEGMENT_TEMPLATE default segment template} looks like this:<br/> * <code>%type.name[ ]%[(]type.plugin[) ]%name</code> * <p> * The <code>%type.name[ ]</code> is rendered as the name of the resource type followed by a space <b>IF</b> the * resource type and the name are not null. If either of them is null <code>%type.name[ ]</code> renders as an * empty string. * <p> * <code>%[(]type.plugin[) ]</code> renders as a bracket followed by the name of the plugin followed by a bracket and * a space if both resource type and plugin are not null. If either of them is null, again the whole <code>%...</code> is * rendered as an empty string. * <p> * The escape character is \. * * @author Lukas Krejci */ public class DisambiguationReportRenderer { public enum RenderingOrder { ASCENDING, DESCENDING } public static final String DEFAULT_SEGMENT_TEMPLATE = "%type.name[ ]%[(]type.plugin[) ]%name"; public static final String DEFALUT_SEGMENT_SEPARATOR = " > "; private boolean includeResource = true; private boolean includeParents = true; private RenderingOrder renderingOrder = RenderingOrder.ASCENDING; private String segmentTemplate = DEFAULT_SEGMENT_TEMPLATE; private String singletonSegmentTemplate = DEFAULT_SEGMENT_TEMPLATE; private String segmentSeparator = DEFALUT_SEGMENT_SEPARATOR; private List<Segment> parsedTemplate = null; private List<Segment> parsedSingletonTemplate = null; /* * The Field, ResourceField and ResourceTypeField constructs are here to * prevent the usage of java reflection. This class is exposed (and probably * used) in the GWT javascript user interface which does not support reflection. * * The below 3 classes are therefore here to mimic what we'd need from reflection * at the cost of lost flexibility and additional verbosity. * * If the DisambiguationReport.Resource or DisambiguationReport.ResourceType classes * change, the below enums will have to accomodate for that change. If we could use * reflection, it would work generically.. */ private interface Field { Object getValue(Object object); Field getRepresentation(); Field getSiblingField(String name); } private enum ResourceField implements Field { ANY("") { public Object getValue(Object object) { return null; } public Field getRepresentation() { return null; } }, ID("id") { public Object getValue(Object object) { return ((Resource)object).getId(); } public Field getRepresentation() { return null; } }, NAME("name") { public Object getValue(Object object) { return ((Resource)object).getName(); } public Field getRepresentation() { return null; } }, TYPE("type") { public Object getValue(Object object) { return ((Resource)object).getType(); } public Field getRepresentation() { return ResourceTypeField.ANY; } }; private String name; private ResourceField(String name) { this.name = name; } public Field getSiblingField(String name) { return getByName(name); } public static ResourceField getByName(String name) { for(ResourceField f : ResourceField.values()) { if (f.name.equals(name)) { return f; } } return null; } } private enum ResourceTypeField implements Field { ANY("") { public Field getRepresentation() { return null; } public Object getValue(Object object) { return null; } }, NAME("name") { public Field getRepresentation() { return null; } public Object getValue(Object object) { return ((ResourceType)object).getName(); } }, PLUGIN("plugin") { public Field getRepresentation() { return null; } public Object getValue(Object object) { return ((ResourceType)object).getPlugin(); } }, SINGLETON("singleton") { public Field getRepresentation() { return null; } public Object getValue(Object object) { return ((ResourceType)object).isSingleton(); } }; private String name; private ResourceTypeField(String name) { this.name = name; } public Field getSiblingField(String name) { return getByName(name); } public static ResourceTypeField getByName(String name) { for(ResourceTypeField f : ResourceTypeField.values()) { if (f.name.equals(name)) { return f; } } return null; } } /** * Segment represents a part of the template with some semantics. * A segment is able to render itself using data from a resource. * * @author Lukas Krejci */ private interface Segment { void render(DisambiguationReport.Resource resource, StringBuilder bld); /** * Each segment holds an internal string builder to build up the data * from the template it will later need to render itself. * * @return the internal string builder. */ StringBuilder getCurrentString(); } private static class TextSegment implements Segment { public StringBuilder text = new StringBuilder(); public void render(DisambiguationReport.Resource resource, StringBuilder bld) { if (text != null) { bld.append(text); } } public StringBuilder getCurrentString() { return text; } } private static class ResourceSegment implements Segment { public String prefix; public List<Field> fields = new ArrayList<Field>(); public String suffix; public StringBuilder currentString = new StringBuilder(); public Field currentField = ResourceField.ANY; public StringBuilder getCurrentString() { return currentString; } public void render(DisambiguationReport.Resource resource, StringBuilder bld) { String value = null; if (fields != null && fields.size() > 0) { Object object = resource; for(Field f : fields) { if (object == null) { break; } object = f.getValue(object); } if (object != null) { value = object.toString(); } } if (value != null) { if (prefix != null) { bld.append(prefix); } bld.append(value.toString()); if (suffix != null) { bld.append(suffix); } } } } private static class SegmentAndState { public Segment segment; public ParserState state; public ParserState lastState; } /** * The guts of the template parser. Each state can process a single character and modify the state for the next * char. */ private enum ParserState { START { public SegmentAndState process(SegmentAndState seg, Character c) { if (c == null) { return null; } seg.lastState = null; switch(c) { case '%': seg.segment = new ResourceSegment(); seg.state = ParserState.IN_RESOURCE_START; break; case '\\': seg.segment = new TextSegment(); seg.lastState = ParserState.IN_TEXT; seg.state = ParserState.ESCAPING; break; default: seg.segment = new TextSegment(); seg.segment.getCurrentString().append(c); seg.state = ParserState.IN_TEXT; break; } return seg; } }, IN_TEXT { public SegmentAndState process(SegmentAndState seg, Character c) { if (c == null) { return null; } seg.lastState = null; switch(c) { case '%': seg.segment = new ResourceSegment(); seg.state = ParserState.IN_RESOURCE_START; break; case '\\': seg.lastState = seg.state; seg.state = ParserState.ESCAPING; break; default: seg.segment.getCurrentString().append(c); break; } return seg; } }, IN_RESOURCE_START { public SegmentAndState process(SegmentAndState seg, Character c) { if (c == null) { return null; } seg.lastState = null; switch(c) { case '[': seg.state = ParserState.IN_RESOURCE_PREFIX; break; default: seg.segment.getCurrentString().append(c); seg.state = ParserState.IN_RESOURCE_DEF; break; } return seg; } }, IN_RESOURCE_PREFIX { public SegmentAndState process(SegmentAndState seg, Character c) { if (c == null) { return null; } seg.lastState = null; switch(c) { case '\\': seg.lastState = seg.state; seg.state = ParserState.ESCAPING; break; case ']': ((ResourceSegment)seg.segment).prefix = seg.segment.getCurrentString().toString(); ((ResourceSegment)seg.segment).currentString = new StringBuilder(); seg.state = ParserState.IN_RESOURCE_DEF; break; default: seg.segment.getCurrentString().append(c); break; } return seg; } }, IN_RESOURCE_DEF { public SegmentAndState process(SegmentAndState seg, Character c) { if (c == null) { processField(seg); return null; } seg.lastState = null; switch(c) { case '\\': seg.lastState = seg.state; seg.state = ParserState.ESCAPING; break; case '.': processField(seg); break; case '%': processField(seg); seg.segment = new ResourceSegment(); seg.state = ParserState.IN_RESOURCE_START; break; case '[': processField(seg); seg.state = ParserState.IN_RESOURCE_SUFFIX; break; default: if (isWhitespace(c)) { processField(seg); seg.segment = new TextSegment(); seg.segment.getCurrentString().append(c); seg.state = ParserState.IN_TEXT; } else { seg.segment.getCurrentString().append(c); } break; } return seg; } //this would have been better implemented if we had reflection in GWT. private void processField(SegmentAndState seg) { ResourceSegment s = (ResourceSegment)seg.segment; String fieldName = s.getCurrentString().toString(); try { Field field = s.currentField.getSiblingField(fieldName); s.fields.add(field); s.currentField = field.getRepresentation(); s.currentString = new StringBuilder(); } catch(Exception e) { s.fields = null; seg.state = ParserState.START; } } private boolean isWhitespace(char c) { //if we had the Character class in GWT... //return Character.isWhitespace(c) return c == ' ' || c == '\n' || c == '\t'; } }, IN_RESOURCE_SUFFIX { public SegmentAndState process(SegmentAndState seg, Character c) { if (c == null) { return null; } seg.lastState = null; switch(c) { case '\\': seg.lastState = seg.state; seg.state = ParserState.ESCAPING; break; case ']': ((ResourceSegment)seg.segment).suffix = seg.segment.getCurrentString().toString(); ((ResourceSegment)seg.segment).currentString = new StringBuilder(); seg.state = ParserState.START; break; default: seg.segment.getCurrentString().append(c); break; } return seg; } }, ESCAPING { public SegmentAndState process(SegmentAndState seg, Character c) { if (c == null) { return null; } seg.segment.getCurrentString().append(c); seg.state = seg.lastState; seg.lastState = null; return seg; } }; public abstract SegmentAndState process(SegmentAndState currentSegment, Character c); } public DisambiguationReportRenderer() { parsedTemplate = parse(segmentTemplate); parsedSingletonTemplate = parse(singletonSegmentTemplate); } /** * @return the includeResource */ public boolean isIncludeResource() { return includeResource; } /** * @param includeResource the includeResource to set */ public void setIncludeResource(boolean includeResource) { this.includeResource = includeResource; } /** * @return the includeParents */ public boolean isIncludeParents() { return includeParents; } /** * @param includeParents the includeParents to set */ public void setIncludeParents(boolean includeParents) { this.includeParents = includeParents; } /** * @return the renderingOrder */ public RenderingOrder getRenderingOrder() { return renderingOrder; } /** * @param renderingOrder the renderingOrder to set */ public void setRenderingOrder(RenderingOrder renderingOrder) { this.renderingOrder = renderingOrder; } /** * @return the segmentTemplate */ public String getSegmentTemplate() { return segmentTemplate; } /** * @param segmentTemplate the segmentTemplate to set */ public void setSegmentTemplate(String segmentTemplate) { this.segmentTemplate = segmentTemplate; parsedTemplate = parse(segmentTemplate); } /** * @return the singletonSegmentTemplate */ public String getSingletonSegmentTemplate() { return singletonSegmentTemplate; } /** * @param singletonSegmentTemplate the singletonSegmentTemplate to set */ public void setSingletonSegmentTemplate(String singletonSegmentTemplate) { this.singletonSegmentTemplate = singletonSegmentTemplate; parsedSingletonTemplate = parse(singletonSegmentTemplate); } /** * @return the segmentSeparator */ public String getSegmentSeparator() { return segmentSeparator; } /** * @param segmentSeparator the segmentSeparator to set */ public void setSegmentSeparator(String segmentSeparator) { this.segmentSeparator = segmentSeparator; } public String render(DisambiguationReport<?> report) { if (parsedTemplate != null) { StringBuilder bld = new StringBuilder(); List<DisambiguationReport.Resource> resources = new ArrayList<DisambiguationReport.Resource>(); switch(renderingOrder) { case ASCENDING: if (includeResource) { resources.add(report.getResource()); } if (includeParents) { resources.addAll(report.getParents()); } break; case DESCENDING: if (includeParents) { ArrayList<DisambiguationReport.Resource> reverseCopy = new ArrayList<DisambiguationReport.Resource>(report.getParents()); Collections.reverse(reverseCopy); resources.addAll(reverseCopy); } if (includeResource) { resources.add(report.getResource()); } break; default:; } String separator = getSegmentSeparator(); for(DisambiguationReport.Resource r : resources) { renderResource(r, bld); bld.append(separator); } if (resources.size() > 0) { bld.replace(bld.length() - separator.length(), bld.length(), ""); } return bld.toString(); } else { return null; } } private List<Segment> parse(String template) { List<Segment> ret = new ArrayList<Segment>(); int idx = 0; SegmentAndState currentState = new SegmentAndState(); currentState.state = ParserState.START; while(idx < template.length()) { char c = template.charAt(idx); Segment lastSegment = currentState.segment; currentState = currentState.state.process(currentState, c); //if the state created a new segment, store it in the results and //continue with it. yes, the reference equality is what we want here if (lastSegment != currentState.segment) { ret.add(currentState.segment); } ++idx; } currentState.state.process(currentState, null); return ret; } private void renderResource(DisambiguationReport.Resource resource, StringBuilder bld) { List<Segment> template = parsedTemplate; if (resource.getType() != null && resource.getType().isSingleton()) { template = parsedSingletonTemplate; } for(Segment seg : template) { seg.render(resource, bld); } } }