class RelatedTopics {

  public List makeModel(TopicIF topic) {
    // first, validate the configuration
    if (excltopics != null && !excltopics.isEmpty() &&
        incltopics != null && !incltopics.isEmpty())
      throw new OntopiaRuntimeException("Configuration fields includeTopicTypes and excludeTopicTypes cannot both be specified.");
    if (exclassocs != null && !exclassocs.isEmpty() &&
        inclassocs != null && !inclassocs.isEmpty())
      throw new OntopiaRuntimeException("Configuration fields includeAssociationTypes and excludeAssociationTypes cannot both be specified.");
        
    // then, update the configuration cache
    updateCache(topic.getTopicMap());

    ParsedQueryIF pquery = null;
    if (filterquery != null)
      pquery = parse(topic.getTopicMap(), filterquery);

    ParsedQueryIF headingOrderQuery = null;
    if (headingOrderQueryString != null)
      headingOrderQuery = parse(topic.getTopicMap(), headingOrderQueryString);
      
    ParsedQueryIF childOrderQuery = null;
    if (childOrderQueryString != null)
      childOrderQuery = parse(topic.getTopicMap(), childOrderQueryString);
    
    // group associations by the headings they will wind up under
    Map typemap = new HashMap();
    Iterator it = getRoles(topic).iterator();
    while (it.hasNext()) {
      AssociationRoleIF role = (AssociationRoleIF) it.next();
      if (isRoleHidden(role, pquery))
        continue; // if the filter hides this association we just skip it

      AssociationIF assoc = role.getAssociation();
      if (getWeakTypes().contains(getObjectId(assoc.getType()))) {
        // this is a weak type
        if (assoc.getRoles().size() != 2)
          throw new OntopiaRuntimeException("Weak associations cannot be " +
                                            "n-ary or unary");
        AssociationRoleIF other = getOtherRole(assoc, role);
        TopicIF player = other.getPlayer();
        TopicIF ttype = null;
        if (player.getTypes().size() > 0)
          ttype = (TopicIF) player.getTypes().iterator().next();
        String key = getObjectId(ttype);
        Heading heading = (Heading) typemap.get(key);
        if (heading == null) {
          heading = new Heading(ttype);
          if (headingOrderQuery != null)
            heading.setSortKey(getSortKey(ttype, headingOrderQuery));
          typemap.put(key, heading);
        }
        Association child = new Association(role, false);
        if (childOrderQuery != null)
          child.setSortKey(getSortKey(child.getPlayer(), childOrderQuery));
        heading.addChild(child);
      } else {
        // not a weak type
        String key = getObjectId(assoc.getType()) + "." +
                     getObjectId(role.getType());
        Heading heading = (Heading) typemap.get(key);
        if (heading == null) {
          heading = new Heading(assoc.getType(), role.getType());
          if (headingOrderQuery != null)
            heading.setSortKey(getSortKey(assoc.getType(), headingOrderQuery));
          typemap.put(key, heading);
        }
        Association child = new Association(role, true);
        if (childOrderQuery != null)
          child.setSortKey(getSortKey(child.getPlayer(), childOrderQuery));
        heading.addChild(child);
      }
    }

    // sort the headings
    List headings = new ArrayList(typemap.values());
    Collections.sort(headings);

    // sort the children
    for (int i=0; i < headings.size(); i++) {
      Heading heading = (Heading)headings.get(i);
      Collections.sort(heading.children);      
    }
    
    // we're done
    return headings;
  }

}