/*
* #!
* Ontopia Navigator
* #-
* Copyright (C) 2001 - 2013 The Ontopia Project
* #-
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* !#
*/
package net.ontopia.topicmaps.nav2.portlets.pojos;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import net.ontopia.topicmaps.core.AssociationIF;
import net.ontopia.topicmaps.core.TopicNameIF;
import net.ontopia.topicmaps.core.OccurrenceIF;
import net.ontopia.topicmaps.core.TopicIF;
import net.ontopia.topicmaps.core.TopicMapBuilderIF;
import net.ontopia.topicmaps.core.TopicMapIF;
import net.ontopia.topicmaps.nav2.portlets.pojos.Menu.Heading;
import net.ontopia.topicmaps.nav2.portlets.pojos.Menu.Item;
import net.ontopia.topicmaps.query.core.DeclarationContextIF;
import net.ontopia.topicmaps.query.core.InvalidQueryException;
import net.ontopia.topicmaps.query.core.ParsedQueryIF;
import net.ontopia.topicmaps.query.core.QueryProcessorIF;
import net.ontopia.topicmaps.query.core.QueryResultIF;
import net.ontopia.topicmaps.query.utils.QueryUtils;
import net.ontopia.utils.OntopiaRuntimeException;
import net.ontopia.utils.StringUtils;
/**
* INTERNAL: Helper methods for Menu.
*/
public class MenuUtils {
// Indicates that a menu item (ItemIF) should be moved up the list.
public static final boolean UP = true;
// Indicates that a menu item (ItemIF) should be moved down the list.
public static final boolean DOWN = false;
/**
* Runs the given query with the given topic as parameter %topic% and returns
* the first value (of the first collumn) in the result collection.
* @param topic The parameter refered to as %topic% in the query.
* @param pq The query that genereates the result.
* @return The first value returned by the query.
*/
public static Object getFirstValue(TopicIF topic, ParsedQueryIF pq) {
QueryResultIF qr = null;
try {
qr = pq.execute(Collections.singletonMap("topic", topic));
while (qr.next()) {
return qr.getValue(0);
}
} catch (InvalidQueryException e) {
throw new OntopiaRuntimeException(e);
} finally {
if (qr != null) qr.close();
}
return null;
}
/**
* Get the values of a given query with a given %topic% parameter as a List
* @param topic The topic parameter represened by %topic% in the query.
* @param parsedQuery The query.
* @return the first result column values of the query, as a List.
*/
public static List getResultValues(TopicIF topic, ParsedQueryIF parsedQuery) {
List topics = new ArrayList();
QueryResultIF qr = null;
try {
qr = parsedQuery.execute(Collections.singletonMap("topic", topic));
while (qr.next()) {
topics.add(qr.getValue(0));
}
} catch (InvalidQueryException e) {
throw new OntopiaRuntimeException(e);
} finally {
if (qr != null) qr.close();
}
return topics;
}
/**
* Test if the given query returns any result rows.
* @param topic The %topic% parameter in the query.
* @param query The query.
* @return true if query returns one or more result rows. Otherwise, false.
*/
public static boolean getResultTrue(TopicIF topic, String query) {
Map params = new HashMap();
params.put("topic", topic);
QueryProcessorIF proc = QueryUtils.getQueryProcessor(topic.getTopicMap());
QueryResultIF qr = null;
try {
qr = proc.execute(query, params);
return qr.next();
} catch (InvalidQueryException e) {
throw new OntopiaRuntimeException(e);
} finally {
if (qr != null)
qr.close();
}
}
/**
* Create new Heading as child of the given parent
* @param topic Represents the parent.
* @param title The title of the Heading.
* @return The Heading that was created.
*/
public static Heading createHeading(TopicIF topic, String title) {
return (Heading)createChild(topic, title, true);
}
/**
* Create new Item as child of the given parent
* @param topic Represents the parent.
* @param title The title of the Item.
* @return The Item that was created.
*/
public static Item createItem(TopicIF topic, String title) {
return (Item)createChild(topic, title, false);
}
/**
* Move the given child one step up or down the list of children on the parent
* @param topic Represents the chils.
* @param up use UP or DOWN to move up or down the list, respectively.
*/
public static void moveOne(TopicIF topic, boolean up) {
TopicMapIF tm = topic.getTopicMap();
QueryProcessorIF qp = QueryUtils.getQueryProcessor(tm);
DeclarationContextIF dc = optimisticParse(tm,
"using menu for i\"http://psi.ontopia.net/portal/menu/\"");
ParsedQueryIF neighbourQuery = optimisticParse(qp, dc,
"select $CSORT, $CVAL from " +
"menu:parent-child($PARENT : menu:parent, %topic% : menu:child), " +
"menu:parent-child($PARENT : menu:parent, $CHILD : menu:child), " +
"occurrence(%topic%, $HSORT), type($HSORT, menu:sort), " +
"occurrence($CHILD, $CSORT), type($CSORT, menu:sort), " +
"value($HSORT, $HVAL), value($CSORT, $CVAL)," +
"$HVAL " + (up ? ">" : "<") + " $CVAL order by $CVAL?");
ParsedQueryIF sortKeyQuery = optimisticParse(qp, dc,
"occurrence(%topic%, $SORT), type($SORT, menu:sort)?");
// Get the sort key of this heading
OccurrenceIF sortKey = (OccurrenceIF)getFirstValue(topic, sortKeyQuery);
// Get the neighbour (previous/next) sort key.
OccurrenceIF neighbourKey = up ? getLastOfQuery(topic, neighbourQuery)
: getFirstOfQuery(topic, neighbourQuery);
// Can't move beyond the extremes of the list.
if (neighbourKey == null)
return;
// Swap sort keys
String swap = sortKey.getValue();
sortKey.setValue(neighbourKey.getValue());
neighbourKey.setValue(swap);
}
/**
* Set the basename of a given topic, removing any old basenames.
* @param topic The topic.
* @param baseName The new basename.
*/
public static void setUniqueTopicName(TopicIF topic, String baseName) {
TopicMapBuilderIF builder = topic.getTopicMap().getBuilder();
Collection oldTopicNames = new HashSet(topic.getTopicNames());
Iterator oldTopicNamesIt = oldTopicNames.iterator();
while (oldTopicNamesIt.hasNext()) {
TopicNameIF currentTopicName = (TopicNameIF)oldTopicNamesIt.next();
currentTopicName.remove();
}
builder.makeTopicName(topic, baseName);
}
/**
* Set the occurrence of a given type on a given topic, removing any existing
* occurrences of the same type on that topic.
* @param topic The topic that should have the occurrence.
* @param typeId The type, as refered to in the query (e.g. "menu:link")
* @param value The value of the occurrence.
*/
public static void setUniqueOccurrence(TopicIF topic, String typeId,
String value) {
String query = "select $LINK from " +
"occurrence(%topic%, $LINK), type($LINK, " + typeId + ")?";
List occs = getResultValues(topic, query);
Iterator occsIt = occs.iterator();
while (occsIt.hasNext()) {
OccurrenceIF occ = (OccurrenceIF)occsIt.next();
occ.remove();
}
TopicMapIF tm = topic.getTopicMap();
TopicIF type = getTopic(typeId, tm);
TopicMapBuilderIF builder = tm.getBuilder();
builder.makeOccurrence(topic, type, value);
}
/**
* Set the binary association with given role types, association types and
* players, removing any existing associations with the same role types and
* association type on player1.
* @param player1 The first player, for which old associations are removed.
* @param rType1Id The first role type, as a string (e.g. "menu:item")
* @param aTypeId The association type, as a string (e.g. "menu:item-topic")
* @param rType2Id The second role type, as a string (e.g. "menu:topic")
* @param player2 The second player.
*/
public static void setUniqueAssociation(TopicIF player1, String rType1Id,
String aTypeId, String rType2Id, TopicIF player2) {
String query = "select $ASSOC from type($ASSOC, " + aTypeId + "), " +
"association-role($ASSOC, $ROLE1), " +
"type($ROLE1, " + rType1Id +"), " +
"role-player($ROLE1, %topic%)?";
List assocs = getResultValues(player1, query);
TopicMapIF tm = player1.getTopicMap();
Iterator assocsIt = assocs.iterator();
while (assocsIt.hasNext()) {
AssociationIF assoc = (AssociationIF)assocsIt.next();
assoc.remove();
}
TopicMapBuilderIF builder = tm.getBuilder();
TopicIF aType = getTopic(aTypeId, tm);
TopicIF rType1 = getTopic(rType1Id, tm);
TopicIF rType2 = getTopic(rType2Id, tm);
AssociationIF assoc = builder.makeAssociation(aType);
builder.makeAssociationRole(assoc, rType1, player1);
builder.makeAssociationRole(assoc, rType2, player2);
}
/**
* Parse the given query for the given topic map.
* InvalidQueryExceptions thrown during the parse process are caught and
* re-thrown with an additional message as OntopiaRuntimeExceptions. This
* avoids external try {} catch() {} blocks around this method.
* @param query The query to parse.
* @param tm The topicmap used by the query.
* @return A ParsedQueryIF representing the parsed query.
*/
protected static ParsedQueryIF optimisticParse(String query, TopicMapIF tm) {
QueryProcessorIF qp = QueryUtils.getQueryProcessor(tm);
DeclarationContextIF dc = optimisticParse(tm,
"using menu for i\"http://psi.ontopia.net/portal/menu/\"");
return optimisticParse(qp, dc, query);
}
/**
* Parse the given declaration-context-query for the given topic map.
* InvalidQueryExceptions thrown during the parse process are caught and
* re-thrown with an additional message as OntopiaRuntimeExceptions. This
* avoids external try {} catch() {} blocks around this method.
* @param tm The topicmap used by the query.
* @param query The query to parse.
* @return A DeclarationContextIF representing the parsed query.
*/
protected static DeclarationContextIF optimisticParse(TopicMapIF tm,
String query) {
try {
return QueryUtils.parseDeclarations(tm, query);
} catch (InvalidQueryException e) {
throw new OntopiaRuntimeException("There was a problem parsing the " +
"query \"" + query +
"\" probably due to an error in the menu ontology.", e);
}
}
/**
* Parse the given declaration-context-query for the given topic map.
* InvalidQueryExceptions thrown during the parse process are caught and
* re-thrown with an additional message as OntopiaRuntimeExceptions. This
* avoids external try {} catch() {} blocks around this method.
* @param qp The query processor that will process the query.
* @param dc A declaration context to be used by the query.
* @param query The query to parse.
* @return A ParsedQueryIF representing the parsed query.
*/
private static ParsedQueryIF optimisticParse(QueryProcessorIF qp,
DeclarationContextIF dc, String query) {
try {
return qp.parse(query, dc);
} catch (InvalidQueryException e) {
throw new OntopiaRuntimeException("There was a problem parsing the " +
"query \"" + query +
"\" probably due to an error in the menu ontology.", e);
}
}
/**
* Runs the given query with the given topic as parameter %topic% and returns
* the last value (of the first collumn) in the result collection.
* @param topic The parameter refered to as %topic% in the query.
* @param query The query that genereates the result.
* @return The last value returned by the query.
*/
private static String getLastValue(TopicIF topic,
ParsedQueryIF query) {
List resultValues = getResultValues(topic, query);
if (resultValues.isEmpty())
return null;
return (String)resultValues.get(resultValues.size() - 1);
}
/**
* Get the result values of the given query with the given %topic% parameter.
* @param topic Parameter represented by %topic% in the query.
* @param query The query.
* @return The first column of the result values.
*/
private static List getResultValues(TopicIF topic, String query) {
TopicMapIF tm = topic.getTopicMap();
ParsedQueryIF pq = optimisticParse(query, tm);
return getResultValues(topic, pq);
}
/**
* Get the topic represented in queries by 'id' from the given topic map.
* @param id The id of the topic as given in a query (e.g. "menu:item")
* @param tm The topic map containing the sought topic.
* @return The topic represented by 'id'
*/
private static TopicIF getTopic(String id, TopicMapIF tm) {
QueryProcessorIF qp = QueryUtils.getQueryProcessor(tm);
QueryResultIF qr = null;
try {
DeclarationContextIF dc = QueryUtils.parseDeclarations(tm,
"using menu for i\"http://psi.ontopia.net/portal/menu/\"");
qr = qp.execute("topic($T), $T = " + id + "?", dc);
if (!qr.next())
throw new OntopiaRuntimeException("Getting topic '" + id +
"' gave no results.");
Object retObject = qr.getValue(0);
if (!(retObject instanceof TopicIF))
throw new OntopiaRuntimeException("Getting topic '" + id + "' should " +
"give a result of type TopicIF, but" +
" gave a result of type " +
retObject.getClass().getName());
TopicIF retTopic = (TopicIF)retObject;
if (qr.next())
throw new OntopiaRuntimeException("Getting topic '" + id +
"' should give a unique result, but" +
" gives more than one result.");
return retTopic;
} catch (InvalidQueryException e) {
throw new OntopiaRuntimeException(e);
} finally {
if (qr != null) qr.close();
}
}
/**
* Get the last value of the first column from the result of a given query.
* @param topic Parameter represented by %topic% in the query.
* @param query The query.
* @return The last value of the first column from the result of the query.
*/
private static OccurrenceIF getLastOfQuery(TopicIF topic,
ParsedQueryIF query) {
List resultValues =
(List)getResultValues(topic, query);
if (resultValues.isEmpty())
return null;
else
return (OccurrenceIF)resultValues.get(resultValues.size() - 1);
}
/**
* Get the first value of the first column from the result of a given query.
* @param topic Parameter represented by %topic% in the query.
* @param query The query.
* @return The first value of the first column from the result of the query.
*/
private static OccurrenceIF getFirstOfQuery(TopicIF topic,
ParsedQueryIF query) {
List resultValues =
(List)getResultValues(topic, query);
if (resultValues.isEmpty())
return null;
else
return (OccurrenceIF)resultValues.get(0);
}
/**
* Create a child (Heading or Item) of the parent represented by a given topic
* @param topic The topic of the parent.
* @param title The title of the new child.
* @param isHeading true to create a Heading, false to create an Item.
* @return
*/
private static Menu.ChildIF createChild(TopicIF topic, String title,
boolean isHeading) {
TopicMapIF tm = topic.getTopicMap();
TopicMapBuilderIF builder = tm.getBuilder();
QueryProcessorIF qp = QueryUtils.getQueryProcessor(tm);
DeclarationContextIF dc = optimisticParse(tm,
"using menu for i\"http://psi.ontopia.net/portal/menu/\"");
TopicIF itemIFTopicType = getTopic(
isHeading ? "menu:heading" : "menu:item", tm);
TopicIF parentChildAssociation = getTopic("menu:parent-child", tm);
TopicIF parentRoleType = getTopic("menu:parent", tm);
TopicIF childRoleType = getTopic("menu:child", tm);
TopicIF sortOccurrenceType = getTopic("menu:sort", tm);
ParsedQueryIF sortKeysQuery = optimisticParse(qp, dc, "select $SORT from " +
"menu:parent-child(%topic% : menu:parent, $CHILD : menu:child), " +
"{ menu:sort($CHILD, $SORT) } order by $SORT?");
// Create the heading topic.
TopicIF itemIFTopic = builder.makeTopic(itemIFTopicType);
// Make the heading child of the menu.
AssociationIF assoc = builder.makeAssociation(parentChildAssociation);
builder.makeAssociationRole(assoc, parentRoleType, topic);
builder.makeAssociationRole(assoc, childRoleType, itemIFTopic);
// Get the highest sort key of children of this menu.
String lastSortKey = getLastValue(topic, sortKeysQuery);
int lastSortKeyInt = Integer.parseInt(lastSortKey);
lastSortKeyInt++;
String newSortKey = StringUtils.pad(lastSortKeyInt, '0', 3);
builder.makeOccurrence(itemIFTopic, sortOccurrenceType, newSortKey);
Menu.ChildIF itemIF;
if (isHeading)
itemIF = new Heading(itemIFTopic);
else
itemIF = new Item(itemIFTopic);
itemIF.setTitle(title);
return itemIF;
}
/**
* Build the Heading of a given topic, from the topic map content.
*/
private static Heading buildHeading(TopicIF topic) {
Heading heading = new Heading(topic);
heading.children = buildChildren(topic);
return heading;
}
/**
* Build the Item of a given topic, from the topic map content.
*/
private static Item buildItem(TopicIF topic) {
TopicMapIF tm = topic.getTopicMap();
ParsedQueryIF itemTopicQuery = MenuUtils.optimisticParse(
"select $TOPIC from " +
"menu:item-topic(%topic% : menu:item, $TOPIC : menu:topic)?", tm);
ParsedQueryIF linkQuery = MenuUtils.optimisticParse(
"select $LINK from menu:link(%topic%, $LINK)?", tm);
ParsedQueryIF imageQuery = MenuUtils.optimisticParse(
"select $IMAGE from menu:image(%topic%, $IMAGE)?", tm);
Item item = new Item(topic);
item.associatedTopic = (TopicIF)MenuUtils
.getFirstValue(topic,itemTopicQuery);
item.link = (String)MenuUtils.getFirstValue(topic, linkQuery);
item.image = (String)MenuUtils.getFirstValue(topic, imageQuery);
String query = item.getCondition();
item.condition = (query == null) || MenuUtils
.getResultTrue(item.associatedTopic, query);
return item;
}
/**
* Build the List of children of a given parent, from the topic map content.
*/
private static List buildChildren(TopicIF topic) {
TopicMapIF tm = topic.getTopicMap();
ParsedQueryIF childrenQuery = MenuUtils.optimisticParse(
"select $CHILD, $SORT from " +
"menu:parent-child(%topic% : menu:parent, $CHILD : menu:child), " +
"{ menu:sort($CHILD, $SORT) } order by $SORT?", tm);
ParsedQueryIF headingsQuery = MenuUtils.optimisticParse(
"select $CHILD from " +
"menu:parent-child(%topic% : menu:parent, $CHILD : menu:child), " +
"instance-of($CHILD, menu:heading)?", tm);
List childrenTopics = MenuUtils.getResultValues(topic, childrenQuery);
List headingsTopics = MenuUtils.getResultValues(topic, headingsQuery);
// populate children
List children = new ArrayList(childrenTopics.size());
for (int i=0; i < childrenTopics.size(); i++) {
TopicIF child = (TopicIF)childrenTopics.get(i);
if (headingsTopics.contains(child))
children.add(buildHeading(child));
else
children.add(buildItem(child));
}
return children;
}
}