/* ******************************************************************************
* Copyright (c) 2006-2012 XMind Ltd. and others.
*
* This file is a part of XMind 3. XMind releases 3 and
* above are dual-licensed under the Eclipse Public License (EPL),
* which is available at http://www.eclipse.org/legal/epl-v10.html
* and the GNU Lesser General Public License (LGPL),
* which is available at http://www.gnu.org/licenses/lgpl.html
* See http://www.xmind.net/license.html for details.
*
* Contributors:
* XMind Ltd. - initial API and implementation
*******************************************************************************/
package org.xmind.core.internal;
import java.util.List;
import org.eclipse.core.expressions.PropertyTester;
import org.eclipse.core.runtime.Assert;
import org.xmind.core.IImage;
import org.xmind.core.ITopic;
import org.xmind.core.internal.xpath.Evaluator;
/**
* A property tester used to test whether a topic contains expected properties.
*
* <p>
* The <code>receiver</code> to test against MUST be an {@link ITopic} instance.
* Otherwise, {@link IllegalArgumentException} will be thrown.
* </p>
*
* <h2>Supported <code>property</code> Values</h2>
*
* <dl>
*
* <dt>eval</dt>
* <dd>Evaluates the topic property evaluation expression (specified by the
* expected value) against the receiver topic. See 'Topic Property Evaluation'
* for details of syntax. The evaluation result MUST be a boolean value.</dd>
*
* <dt>type</dt>
* <dd>Compares the expected value against the type of the receiver topic
* retrieved via {@link ITopic#getType()}. This property is a shortcut of
* testing the <code>eval</code> property with 'args' specified as
* <code>matches(@type,'EXPECTED_VALUE')</code> if the expected value starts
* with <code>"^"</code> or <code>@type='EXPECTED_VALUE'</code> otherwise.</dd>
*
* <dt>title</dt>
* <dd>Compares the expected value against the topic title text retrieved via
* {@link ITopic#getTitleText()}. This property is a shortcut of testing the
* <code>eval</code> property with 'args' specified as
* <code>matches(@title,'EXPECTED_VALUE')</code> if the expected value starts
* with <code>"^"</code> or <code>@title='EXPECTED_VALUE'</code> otherwise.</dd>
*
* <dt>structureClass</dt>
* <dd>Compares the expected value against the structure class retrieved via
* {@link ITopic#getStructureClass()}. This property is a shortcut of testing
* the <code>eval</code> property with 'args' specified as
* <code>matches(@structureClass,'EXPECTED_VALUE')</code> if the expected value
* starts with <code>"^"</code> or <code>@structureClass='EXPECTED_VALUE'</code>
* otherwise.</dd>
*
* <dt>folded</dt>
* <dd>Compares the expected value against the folded state retrieved via
* {@link ITopic#isFolded()}. This property is a shortcut of testing the
* <code>eval</code> property with 'args' specified as <code>@folded</code>.
* </dd>
*
* <dt>hyperlink</dt>
* <dd>Compares the expected value against the hyperlink value retrieved via
* {@link ITopic#getHyperlink()}. This property is a shortcut of testing the
* <code>eval</code> property with 'args' specified as
* <code>matches(@hyperlink,'EXPECTED_VALUE')</code> if the expected value
* starts with <code>"^"</code> or <code>@hyperlink='EXPECTED_VALUE'</code>
* otherwise.</dd>
*
* <dt>imageSource</dt>
* <dd>Compares the expected value against the image source retrieved via
* {@link ITopic#getImage()} and {@link IImage#getSource()}. This property is a
* shortcut of testing the <code>eval</code> property with 'args' specified as
* <code>matches(image/@source,'EXPECTED_VALUE')</code> if the expected value
* starts with <code>"^"</code> or <code>image/@source='EXPECTED_VALUE'</code>
* otherwise.</dd>
*
* </dl>
*
* <h2>Topic Property Evaluation</h2>
*
* <p>
* A topic property evaluation expression is a function call (e.g.
* <code>matches(@type,'(de|at)tached')</code>), a comparison (e.g.
* <code>@type!='attached'</code>) or an evaluation of a boolean value (e.g.
* <code>@folded</code>). Variables may be an XPath-like query (e.g.
* <code>topic[@type='attached']/label/text()</code>), a string value (e.g.
* <code>'xxx'</code>), a number value (e.g. <code>23</code>) or a boolean value
* (e.g. <code>true()</code> or <code>false()</code>).
* </p>
*
* <p>
* An XPath-like query is evaluated against the receiver topic, so it starts
* <em>without</em> a leading slash (<code>/</code>) to indicate that this is a
* relative path. See the following Topic Model Hierarchy for what elements and
* attributes can be selected.
* </p>
*
* <p>
* This class only supports a small set of XPath 2 specifications.
* </p>
*
* <dl>
* <dt><em>label</em></dt>
* <dd>(Collection) Returns the child element labeled by the specified label
* </dd>
* <dt><em>@attr</em></dt>
* <dd>(String, Boolean, Number) Returns the specified attribute value of the
* context object (or the first object of the context collection)</dd>
* <dt><em>x</em>=<em>y</em>, <em>x</em>!=<em>y</em>, <em>x</em>><em>y</em>,
* <em>x</em><<em>y</em>, <em>x</em>>=<em>y</em>, <em>x</em><=
* <em>y</em></dt>
* <dd>(Boolean) Compares <em>x</em> and <em>y</em> using the literal operator
* </dd>
* <dt>count(Collection <em>node-set</em>)</dt>
* <dd>(Number) Returns the count of the nodes in the specified node set</dd>
* <dt>matches(String <em>str</em>, String <em>pattern</em>)</dt>
* <dd>(Boolean) Returns whether the string matches the specified regular
* expression pattern</dd>
* <dt>true()</dt>
* <dd>(Boolean) Returns <em>true</em></dd>
* <dt>false()</dt>
* <dd>(Boolean) Returns <em>false</em></dd>
* </dl>
*
* <h2>Topic Model Hierarchy</h2>
*
* <dl>
*
* <dt>topic</dt>
* <dd>
* <dl>
* <dt>Type</dt>
* <dd>{@link org.xmind.core.ITopic}</dd>
* <dt>Attributes</dt>
* <dd>
* <dl>
* <dt>type</dt>
* <dd>(String) {@link org.xmind.core.ITopic#getType()}</dd>
* <dt>title</dt>
* <dd>(String) {@link org.xmind.core.ITopic#getTitleText()}</dd>
* <dt>structureClass</dt>
* <dd>(String) {@link org.xmind.core.ITopic#getStructureClass()}</dd>
* <dt>folded</dt>
* <dd>(boolean) {@link org.xmind.core.ITopic#isFolded()}</dd>
* <dt>hyperlink</dt>
* <dd>(String) {@link org.xmind.core.ITopic#getHyperlink()}</dd>
* </dl>
* </dd>
* <dt>Elements</dt>
* <dd>
* <dl>
* <dt>image</dt>
* <dd>A singleton list of {@link org.xmind.core.ITopic#getImage()}</dd>
* <dt>marker</dt>
* <dd>(Set) {@link org.xmind.core.ITopic#getMarkerRefs()}</dd>
* <dt>label</dt>
* <dd>(Set) {@link org.xmind.core.ITopic#getLabels()}</dd>
* <dt>extension</dt>
* <dd>(List) {@link org.xmind.core.ITopic#getExtension(String)}</dd>
* <dt>topic</dt>
* <dd>(List) {@link org.xmind.core.ITopic#getAllChildren()}</dd>
* </dl>
* </dd>
* </dl>
* </dd>
*
* <dt>image</dt>
* <dd>
* <dl>
* <dt>Type</dt>
* <dd>{@link org.xmind.core.IImage}</dd>
* <dt>Attributes</dt>
* <dd>
* <dl>
* <dt>source</dt>
* <dd>(String) {@link org.xmind.core.IImage#getSource()}</dd>
* </dl>
* </dd>
* </dl>
* </dd>
*
* <dt>marker</dt>
* <dd>
* <dl>
* <dt>Type</dt>
* <dd>{@link org.xmind.core.marker.IMarkerRef}</dd>
* <dt>Attributes</dt>
* <dd>
* <dl>
* <dt>id</dt>
* <dd>(String) {@link org.xmind.core.marker.IMarkerRef#getMarkerId()}</dd>
* <dt>name</dt>
* <dd>(String) {@link org.xmind.core.marker.IMarkerRef#getDescription()}</dd>
* <dt>groupId</dt>
* <dd>(String) The marker group id retrieved by
* <code>element.getMarker().getParent().getId()</code></dd>
* </dl>
* </dd>
* </dl>
* </dd>
*
* <dt>label</dt>
* <dd>
* <dl>
* <dt>Type</dt>
* <dd>String</dd>
* <dt>Text Content (<code>text()</code>)</dt>
* <dd>The string value of this label</dd>
* </dl>
* </dd>
*
* <dt>extension</dt>
* <dd>
* <dl>
* <dt>Type</dt>
* <dd>{@link org.xmind.core.ITopicExtension}</dd>
* <dt>Attributes</dt>
* <dd>
* <dl>
* <dt>provider</dt>
* <dd>(String) {@link org.xmind.core.ITopicExtension#getProviderName()}</dd>
* </dl>
* </dd>
* <dt>Elements</dt>
* <dd>
* <dl>
* <dt>content</dt>
* <dd>A singleton collection of
* {@link org.xmind.core.ITopicExtension#getContent()}</dd>
* <dt>resource</dt>
* <dd>(List) {@link org.xmind.core.ITopicExtension#getResourceRefs()}</dd>
* </dl>
* </dd>
* </dl>
* </dd>
*
* <dt>resource</dt>
* <dd>
* <dl>
* <dt>Type</dt>
* <dd>{@link org.xmind.core.IResourceRef}</dd>
* <dt>Attributes</dt>
* <dd>
* <dl>
* <dt>type</dt>
* <dd>(String) {@link org.xmind.core.IResourceRef#getType()}</dd>
* <dt>id</dt>
* <dd>(String) {@link org.xmind.core.IResourceRef#getResourceId()}</dd>
* </dl>
* </dd>
* </dl>
* </dd>
*
* <dt>content</dt>
* <dd>
* <dl>
* <dt>Type</dt>
* <dd>{@link org.xmind.core.ITopicExtensionElement}</dd>
* <dt>Attributes</dt>
* <dd>(String)
* {@link org.xmind.core.ITopicExtensionElement#getAttribute(String)}</dd>
* <dt>Elements</dt>
* <dd>(List) {@link org.xmind.core.ITopicExtensionElement#getChildren(String)}
* </dd>
* <dt>Text Content</dt>
* <dd>(String) {@link org.xmind.core.ITopicExtensionElement#getTextContent()}
* </dd>
* </dl>
* </dd>
* </dl>
*
* @author Frank Shaka
*
*/
public class TopicPropertyTester extends PropertyTester {
private static final String P_EVAL = "eval"; //$NON-NLS-1$
private static final String P_TYPE = "type"; //$NON-NLS-1$
private static final String P_TITLE = "title"; //$NON-NLS-1$
private static final String P_STRUCTURE_CLASS = "structureClass"; //$NON-NLS-1$
private static final String P_FOLDED = "folded"; //$NON-NLS-1$
private static final String P_HYPERLINK = "hyperlink"; //$NON-NLS-1$
private static final String P_IMAGE_SOURCE = "imageSource"; //$NON-NLS-1$
public boolean test(Object receiver, String property, Object[] args,
Object expectedValue) {
Assert.isLegal(receiver instanceof ITopic,
"Receiver is not an ITopic object: " + receiver); //$NON-NLS-1$
ITopic topic = (ITopic) receiver;
if (P_TYPE.equals(property)) {
return evaluates(topic, propEvalExp("@type", expectedValue)); //$NON-NLS-1$
} else if (P_HYPERLINK.equals(property)) {
return evaluates(topic, propEvalExp("@hyperlink", expectedValue)); //$NON-NLS-1$
} else if (P_TITLE.equals(property)) {
return evaluates(topic, propEvalExp("@title", expectedValue)); //$NON-NLS-1$
} else if (P_STRUCTURE_CLASS.equals(property)) {
return evaluates(topic,
propEvalExp("@structureClass", expectedValue)); //$NON-NLS-1$
} else if (P_FOLDED.equals(property)) {
return evaluates(topic, "@folded"); //$NON-NLS-1$
} else if (P_IMAGE_SOURCE.equals(property)) {
return evaluates(topic,
propEvalExp("image/@source", expectedValue)); //$NON-NLS-1$
} else if (P_EVAL.equals(property)) {
if (!(expectedValue instanceof String))
return false;
return evaluates(topic, (String) expectedValue);
}
throw new IllegalArgumentException(
"Unrecognized property: " + property); //$NON-NLS-1$
}
private static String propEvalExp(String propertyPath,
Object expectedValue) {
String value = expectedValue == null ? "" : expectedValue.toString(); //$NON-NLS-1$
if (value.startsWith("^")) //$NON-NLS-1$
return String.format("matches(%s,'%s')", propertyPath, value); //$NON-NLS-1$
return String.format("%s='%s'", propertyPath, value); //$NON-NLS-1$
}
private static boolean evaluates(ITopic topic, String expression) {
Evaluator evaluator = new Evaluator(expression, new CoreAxisProvider());
List<Object> sequence = evaluator.evaluate(topic);
if (sequence.isEmpty())
return false;
Object result = sequence.get(0);
if (result instanceof Boolean)
return ((Boolean) result).booleanValue();
return result != null;
}
}