gt:             String desc = concept.getDescription();
context: package rres.knetminer.datasource.ondexlocal;
  
  import java.awt.Font;
  import java.beans.XMLEncoder;
  import java.io.BufferedReader;
  import java.io.BufferedWriter;
  import java.io.ByteArrayOutputStream;
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileOutputStream;
  import java.io.FileWriter;
  import java.io.IOException;
  import java.io.InputStreamReader;
  import java.io.ObjectInputStream;
  import java.io.ObjectOutputStream;
  import java.io.OutputStreamWriter;
  import java.io.UnsupportedEncodingException;
  import java.net.URL;
  import java.net.URLEncoder;
  import java.nio.charset.Charset;
  import java.nio.file.Files;
  import java.nio.file.Paths;
  import java.text.DecimalFormat;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.Collection;
  import java.util.Comparator;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  import java.util.Random;
  import java.util.Set;
  import java.util.SortedMap;
  import java.util.SortedSet;
  import java.util.TreeMap;
  import java.util.TreeSet;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  import java.util.zip.GZIPOutputStream;
  
  import net.sourceforge.ondex.core.Attribute;
  import org.apache.commons.io.FileUtils;
  import org.apache.logging.log4j.LogManager;
  import org.apache.logging.log4j.Logger;
  import org.apache.lucene.analysis.Analyzer;
  //import org.apache.lucene.analysis.WhitespaceAnalyzer;
  import org.apache.lucene.analysis.core.WhitespaceAnalyzer;
  import org.apache.lucene.analysis.standard.StandardAnalyzer;
  //import org.apache.lucene.queryParser.ParseException;
  import org.apache.lucene.queryparser.classic.ParseException;
  //import org.apache.lucene.queryParser.QueryParser;
  import org.apache.lucene.queryparser.classic.QueryParser;
  import org.apache.lucene.search.BooleanClause;
  import org.apache.lucene.search.BooleanQuery;
  import org.apache.lucene.search.Query;
  
  import net.sourceforge.ondex.InvalidPluginArgumentException;
  import net.sourceforge.ondex.ONDEXPluginArguments;
  import net.sourceforge.ondex.algorithm.graphquery.GraphTraverser;
  import net.sourceforge.ondex.algorithm.graphquery.StateMachine;
  import net.sourceforge.ondex.algorithm.graphquery.flatfile.StateMachineFlatFileParser2;
  import net.sourceforge.ondex.algorithm.graphquery.nodepath.EvidencePathNode;
  import net.sourceforge.ondex.args.FileArgumentDefinition;
  import net.sourceforge.ondex.config.ONDEXGraphRegistry;
  import net.sourceforge.ondex.core.AttributeName;
  import net.sourceforge.ondex.core.ConceptAccession;
  import net.sourceforge.ondex.core.ConceptClass;
  import net.sourceforge.ondex.core.ConceptName;
  import net.sourceforge.ondex.core.EvidenceType;
  import net.sourceforge.ondex.core.ONDEXConcept;
  import net.sourceforge.ondex.core.ONDEXGraph;
  import net.sourceforge.ondex.core.ONDEXGraphMetaData;
  import net.sourceforge.ondex.core.ONDEXRelation;
  import net.sourceforge.ondex.core.RelationType;
  import net.sourceforge.ondex.core.memory.MemoryONDEXGraph;
  import net.sourceforge.ondex.core.searchable.LuceneConcept;
  import net.sourceforge.ondex.core.searchable.LuceneEnv;
  import net.sourceforge.ondex.core.searchable.ScoredHits;
  import net.sourceforge.ondex.exception.type.PluginConfigurationException;
  //import net.sourceforge.ondex.export.oxl.Export;
  import net.sourceforge.ondex.export.cyjsJson.Export;
  import net.sourceforge.ondex.filter.unconnected.ArgumentNames;
  import net.sourceforge.ondex.filter.unconnected.Filter;
  import net.sourceforge.ondex.parser.oxl.Parser;
  import net.sourceforge.ondex.tools.ondex.ONDEXGraphCloner;
  import rres.knetminer.datasource.api.QTL;
  import rres.knetminer.datasource.ondexlocal.FisherExact;
  
  /**
   * Parent class to all ondex service provider classes implementing organism
   * specific searches.
   *
   * @author taubertj, pakk, singha
   */
  public class OndexServiceProvider {
      protected final Logger log = LogManager.getLogger(getClass());
  
      /**
       * ChromosomeID mapping for different datasets
       */
      // BidiMap<Integer, String> chromBidiMap = new DualHashBidiMap<Integer,
      // String>();
  
      /**
       * GraphTraverser will be initiated with a state machine
       */
      GraphTraverser gt;
  
      /**
       * Ondex knowledge base as memory graph
       */
      ONDEXGraph graph;
  
      /**
       * Query-independent Ondex motifs as a hash map
       */
      static HashMap<Integer, Set<Integer>> mapGene2Concepts;
      static HashMap<Integer, Set<Integer>> mapConcept2Genes;
  
      /**
       * HashMap of geneID -> endNodeID_pathLength
       */
      static HashMap<String, Integer> mapGene2PathLength;
  
      /**
       * Query-dependent mapping between genes and concepts that contain query terms
       */
      HashMap<Integer, Set<Integer>> mapGene2HitConcept;
  
      /**
       * Gene to QTL mapping
       */
      HashMap<Integer, Set<Integer>> mapGene2QTL;
  
      /**
       * Temp: map query genes to a score
       */
  
      HashMap<ONDEXConcept, Double> scoredCandidates;
  
      /**
       * number of genes in genome
       */
      int numGenesInGenome;
  
      /**
       * index the graph
       */
      LuceneEnv lenv;
  
      /**
       * TaxID of organism for which the knowledgebase was created
       */
      List<String> taxID;
  
      /**
       * true if a reference genome is provided
       */
      boolean referenceGenome;
  
      boolean export_visible_network;
  
  
      /**
       * Loads configuration for chromosomes and initialises map
       */
      OndexServiceProvider() {
  
      }
  
      /**
       * Load OXL data file into memory Build Lucene index for the Ondex graph Create
       * a state machine for semantic motif search
       *
       * @throws ArrayIndexOutOfBoundsException
       * @throws PluginConfigurationException
       */
      public void createGraph(String dataPath, String graphFileName, String smFileName)
              throws ArrayIndexOutOfBoundsException, PluginConfigurationException {
  
          log.info("Loading graph from " + graphFileName);
  
          // new in-memory graph
          graph = new MemoryONDEXGraph("OndexKB");
  
          loadOndexKBGraph(graphFileName);
          indexOndexGraph(graphFileName, dataPath);
  
          log.info("Loading semantic motifs from " + smFileName);
          StateMachine sm = loadSemanticMotifs(smFileName);
  
          // create a crawler using our semantic motifs
          gt = new GraphTraverser(sm);
  
          populateHashMaps(graphFileName, dataPath);
  
          // determine number of genes in given species (taxid)
          AttributeName attTAXID = graph.getMetaData().getAttributeName("TAXID");
          ConceptClass ccGene = graph.getMetaData().getConceptClass("Gene");
          Set<ONDEXConcept> seed = graph.getConceptsOfConceptClass(ccGene);
  
          for (ONDEXConcept gene : seed) {
              if (gene.getAttribute(attTAXID) != null
                      && taxID.contains(gene.getAttribute(attTAXID).getValue().toString())) {
                  numGenesInGenome++;
              }
          }
  
          // Write Stats about the created Ondex graph & its mappings to a file.
          log.info("Saving graph stats to " + dataPath);
          displayGraphStats(dataPath);
  
          log.info("Done loading " + graphFileName + ". Waiting for queries...");
      }
  
      /*
       * Generate Stats about the created Ondex graph and its mappings:
       * mapConcept2Genes & mapGene2Concepts. author singha
       */
      private void displayGraphStats(String fileUrl) {
          // Update the Network Stats file that holds the latest Stats
          // information.
          String fileName = Paths.get(fileUrl, "latestNetwork_Stats.tab").toString();
  
          int minValues, maxValues = 0, avgValues, all_values_count = 0;
  
          // Also, create a timetamped Stats file to retain historic Stats
          // information.
          long timestamp = System.currentTimeMillis();
          String newFileName = Paths.get(fileUrl, timestamp + "_Network_Stats.tab").toString();
          try {
              int totalGenes = numGenesInGenome;
              int totalConcepts = graph.getConcepts().size();
              int totalRelations = graph.getRelations().size();
              int geneEvidenceConcepts = mapConcept2Genes.size();
              minValues = geneEvidenceConcepts > 0
                      ? mapGene2Concepts.get(mapGene2Concepts.keySet().toArray()[0]).size()
                      : 0; // initial value
              /*
               * Get the min., max. & average size (no. of values per key) for the
               * gene-evidence network (in the mapGene2Concepts HashMap.
               */
              Set<Map.Entry<Integer, Set<Integer>>> set = mapGene2Concepts.entrySet(); // dataMap.entrySet();
              Iterator<Map.Entry<Integer, Set<Integer>>> iterator = set.iterator();
              while (iterator.hasNext()) {
                  Map.Entry<Integer, Set<Integer>> mEntry = iterator.next();
                  Set<Integer> value = mEntry.getValue(); // Value HashSet<Integer>).
                  int number_of_values = value.size(); // size of the values
  
                  if (number_of_values < minValues) {
                      minValues = number_of_values;
                  }
                  if (number_of_values > maxValues) {
                      maxValues = number_of_values;
                  }
                  // Retain the sum of sizes of all the key-value pairs in the
                  // HashMap.
                  all_values_count = all_values_count + number_of_values;
              }
  
              // Total no. of keys in the HashMap.
              int all_keys = mapGene2Concepts.keySet().size();
              // Calculate average size of gene-evidence networks in the HashMap.
              avgValues = all_keys > 0 ? all_values_count / all_keys : 0;
  
              /*
               * log.info("Graph Stats:");
               * log.info("1) Total number of genes: "+ totalGenes);
               * log.info("2) Total concepts: "+ totalConcepts);
               * log.info("3) Total Relations: "+ totalRelations);
               * log.info("4) Concept2Gene #mappings: "+ geneEvidenceConcepts);
               * log.info("5) Min., Max., Average size of gene-evidence networks: "
               * + minValues +", "+ maxValues +", "+ avgValues);
               */
  
              // Write the Stats to a .tab file.
              StringBuffer sb = new StringBuffer();
              // sb.append("<?xml version=\"1.0\" standalone=\"yes\"?>\n");
              sb.append("<stats>\n");
              sb.append("<totalGenes>").append(totalGenes).append("</totalGenes>\n");
              sb.append("<totalConcepts>").append(totalConcepts).append("</totalConcepts>\n");
              sb.append("<totalRelations>").append(totalRelations).append("</totalRelations>\n");
              sb.append("<geneEvidenceConcepts>").append(geneEvidenceConcepts).append("</geneEvidenceConcepts>\n");
              sb.append("<evidenceNetworkSizes>\n");
              sb.append("<minSize>").append(minValues).append("</minSize>\n");
              sb.append("<maxSize>").append(maxValues).append("</maxSize>\n");
              sb.append("<avgSize>").append(avgValues).append("</avgSize>\n");
              sb.append("</evidenceNetworkSizes>\n");
  
              // Display table breakdown of all conceptClasses in network
              sb.append("<conceptClasses>\n");
              Set<ConceptClass> conceptClasses = graph.getMetaData().getConceptClasses(); // get all concept classes
              Set<ConceptClass> sorted_conceptClasses = new TreeSet<ConceptClass>(conceptClasses); // sorted
              for (ConceptClass con_class : sorted_conceptClasses) {
                  if (graph.getConceptsOfConceptClass(con_class).size() > 0) {
                      String conID = con_class.getId();
                      int con_count = graph.getConceptsOfConceptClass(con_class).size();
                      if (conID.equalsIgnoreCase("Path")) {
                          conID = "Pathway";
                      } else if (conID.equalsIgnoreCase("Comp")) {
                          conID = "Compound";
                      } else if (conID.equalsIgnoreCase("Trait")) {
                          conID = "Trait (GWAS)";
                      } else if (conID.equalsIgnoreCase("Gene")) {
                          con_count = numGenesInGenome;
                      }
  					// exclude "Thing" CC
                      if(!conID.equalsIgnoreCase("Thing")) {
  					   sb.append("<cc_count>").append(conID).append("=").append(con_count).append("</cc_count>\n");
  					  }
                  }
              }
              sb.append("</conceptClasses>\n");
              sb.append("</stats>");
  
              // Update the file storing the latest Stats data.
              BufferedWriter out = new BufferedWriter(new FileWriter(fileName));
              out.write(sb.toString()); // write contents.
              out.close();
  
              // Also, create the timestamped Stats file.
              BufferedWriter out2 = new BufferedWriter(new FileWriter(newFileName));
              out2.write(sb.toString()); // write contents.
              out2.close();
  
              /*
               * generate gene2evidence .tab file with contents of the mapGenes2Concepts
               * HashMap & evidence2gene .tab file with contents of the mapConcepts2Genes
               * HashMap
               */
              //   generateGeneEvidenceStats(fileUrl); // DISABLED
  
          } catch (IOException e) {
              log.error("Failed to count stats for graph", e);
          }
      }
  
      /**
       * Loads OndexKB Graph (OXL data file into memory)
       */
      private void loadOndexKBGraph(String filename) {
          try {
              log.debug("Start Loading OndexKB Graph..." + filename);
              Parser oxl = new Parser();
              ONDEXPluginArguments pa = new ONDEXPluginArguments(oxl.getArgumentDefinitions());
              pa.setOption(FileArgumentDefinition.INPUT_FILE, filename);
              oxl.setArguments(pa);
              oxl.setONDEXGraph(graph);
  
              oxl.start();
              log.debug("OndexKB Graph Loaded Into Memory");
          } catch (Exception e) {
              log.error("Failed to load graph", e);
          }
      }
  
      /**
       * Indexes Ondex Graph
       */
      private void indexOndexGraph(String graphFileName, String dataPath) {
          try {
              // index the Ondex graph
              File graphFile = new File(graphFileName);
              File indexFile = Paths.get(dataPath, "index").toFile();
              if (indexFile.exists() && (indexFile.lastModified() < graphFile.lastModified())) {
                  log.info("Graph file updated since index last built, deleting old index");
                  FileUtils.deleteDirectory(indexFile);
              }
              log.debug("Building Lucene Index: " + indexFile.getAbsolutePath());
              if (!indexFile.exists())
                  lenv = new LuceneEnv(indexFile.getAbsolutePath(), true);
              else
                  lenv = new LuceneEnv(indexFile.getAbsolutePath(), false);
              lenv.setONDEXGraph(graph);
              log.debug("Lucene Index created");
          } catch (Exception e) {
              log.error("Faild to load graph index", e);
          }
      }
  
      /**
       * Creates a state machine for semantic motif search
       */
      private StateMachine loadSemanticMotifs(String smFile) {
  
          StateMachineFlatFileParser2 smp = null;
          try {
              // load semantic motifs from file
              URL motifsUrl = Thread.currentThread().getContextClassLoader().getResource(smFile);
  
              smp = new StateMachineFlatFileParser2();
              smp.parseReader(new BufferedReader(new InputStreamReader(motifsUrl.openStream())), graph);
  
              log.debug("Completed State Machine");
          } catch (Exception e) {
              log.error("Failed to compile state machine", e);
          }
  
          return smp.getStateMachine();
      }
  
      /**
       * Export the Ondex graph to file system as a .oxl file and also in JSON format
       * using the new JSON Exporter plugin in Ondex.
       *
       * @param ONDEXGraph graph
       * @throws InvalidPluginArgumentException
       */
      public String exportGraph(ONDEXGraph og) throws InvalidPluginArgumentException {
          // Unconnected filter
          Filter uFilter = new Filter();
          ONDEXPluginArguments uFA = new ONDEXPluginArguments(uFilter.getArgumentDefinitions());
          uFA.addOption(ArgumentNames.REMOVE_TAG_ARG, true);
  
          // TODO
          uFA.addOption(ArgumentNames.CONCEPTCLASS_RESTRICTION_ARG, "Publication");
          uFA.addOption(ArgumentNames.CONCEPTCLASS_RESTRICTION_ARG, "Chromosome");
  
          uFilter.setArguments(uFA);
          uFilter.setONDEXGraph(og);
          uFilter.start();
  
          ONDEXGraph graph2 = new MemoryONDEXGraph("FilteredGraphUnconnected");
          uFilter.copyResultsToNewGraph(graph2);
  
          // // Attribute Value Filter
          // net.sourceforge.ondex.filter.attributevalue.Filter visFilter = new
          // net.sourceforge.ondex.filter.attributevalue.Filter();
          // ONDEXPluginArguments visFA = new
          // ONDEXPluginArguments(visFilter.getArgumentDefinitions());
          // visFA.addOption(net.sourceforge.ondex.filter.attributevalue.ArgumentNames.ATTRNAME_ARG,
          // "size");
          // //Not sure if "true" needs to be string or boolean
          // visFA.addOption(net.sourceforge.ondex.filter.attributevalue.ArgumentNames.VALUE_ARG,
          // "0");
          // visFA.addOption(net.sourceforge.ondex.filter.attributevalue.ArgumentNames.OPERATOR_ARG,
          // ">");
          // visFA.addOption(net.sourceforge.ondex.filter.attributevalue.ArgumentNames.INCLUDING_ARG,
          // true);
          // visFA.addOption(net.sourceforge.ondex.filter.attributevalue.ArgumentNames.IGNORE_ARG,
          // true);
          //
          // visFilter.setArguments(visFA);
          // visFilter.setONDEXGraph(graph2);
          // try {
          // visFilter.start();
          // } catch (WrongArgumentException e1) {
          // // TODO Auto-generated catch block
          // e1.printStackTrace();
          // }
          //
          // ONDEXGraph graph3 = new MemoryONDEXGraph("FilteredGraph");
          // visFilter.copyResultsToNewGraph(graph3);
  
          // oxl export
          // disabled (25/10/17)
          /*
           * Export export = new Export(); export.setLegacyMode(true);
           * ONDEXPluginArguments ea = new
           * ONDEXPluginArguments(export.getArgumentDefinitions());
           * ea.setOption(FileArgumentDefinition.EXPORT_FILE, exportPath);
           * ea.addOption("GZip", true); export.setArguments(ea);
           * export.setONDEXGraph(graph2); try { export.start(); } catch (IOException e) {
           * e.printStackTrace(); System.out.println(e.getMessage()); } catch
           * (XMLStreamException e) { e.printStackTrace();
           * System.out.println(e.getMessage()); } catch (JAXBException e) {
           * e.printStackTrace(); System.out.println(e.getMessage()); }
           *
           * // Check if .oxl file exists while (!fileIsCreated) { fileIsCreated =
           * checkFileExist(exportPath); } System.out.println("OXL file created:" +
           * exportPath);
           */
  
          // Export the graph as JSON too, using the Ondex JSON Exporter plugin.
          Export jsonExport = new Export();
          // JSON output file.
          // String jsonExportPath = exportPath.substring(0, exportPath.length() - 4) +
          // ".json";
          byte[] encoded = new byte[0];
          try {
              File exportPath = File.createTempFile("knetminer", "graph");
              exportPath.deleteOnExit(); // Just in case we don't get round to deleting it ourselves
              ONDEXPluginArguments epa = new ONDEXPluginArguments(jsonExport.getArgumentDefinitions());
              epa.setOption(FileArgumentDefinition.EXPORT_FILE, exportPath.getAbsolutePath());
  
              log.debug("JSON Export file: " + epa.getOptions().get(FileArgumentDefinition.EXPORT_FILE));
  
              jsonExport.setArguments(epa);
              jsonExport.setONDEXGraph(graph2);
              log.debug("Export JSON data: Total concepts= " + graph2.getConcepts().size() + " , Relations= "
                      + graph2.getRelations().size());
              // Export the contents of the 'graph' object as multiple JSON
              // objects to an output file.
              jsonExport.start();
              log.debug("Network JSON file created:" + /* jsonExportPath */exportPath.getAbsolutePath());
              encoded = Files.readAllBytes(exportPath.toPath());
              exportPath.delete();
          } catch (IOException ex) {
              log.error("Failed to export graph", ex);
          }
          return new String(encoded, Charset.defaultCharset());
      }
  
      // JavaScript Document
  
      /**
       * Creates a new keyword for finding the NOT list
       *
       * @param keyword original keyword
       * @return new keyword for searching the NOT list
       */
      private String createsNotList(String keyword) {
          String result = "";
          if (keyword == null) {
              keyword = "";
          }
  
          keyword = keyword.replace("(", "");
          keyword = keyword.replace(")", "");
  
          keyword = keyword.replaceAll("OR", "__");
          keyword = keyword.replaceAll("AND", "__");
  
          String[] keySplitedOrAnd = keyword.split("__");
          for (String keyOA : keySplitedOrAnd) {
              String[] keySplitedNOT = keyOA.split("NOT");
              int notCount = 0;
              for (String keyN : keySplitedNOT) {
                  if (notCount > 0) {
                      result = result + keyN + " OR ";
                  }
                  notCount++;
              }
          }
          if (result != "") {
              result = result.substring(0, (result.length() - 3));
          }
          return result;
      }
  
      /**
       * Merge two maps using the greater scores
       *
       * @param hit2score map that holds all hits and scores
       * @param sHits     map that holds search results
       */
      private void mergeHits(HashMap<ONDEXConcept, Float> hit2score, ScoredHits<ONDEXConcept> sHits,
                             ScoredHits<ONDEXConcept> NOTHits) {
          for (ONDEXConcept c : sHits.getOndexHits()) {
              if (NOTHits == null || !NOTHits.getOndexHits().contains(c)) {
                  if (c instanceof LuceneConcept) {
                      c = ((LuceneConcept) c).getParent();
                  }
                  if (!hit2score.containsKey(c)) {
                      hit2score.put(c, sHits.getScoreOnEntity(c));
                  } else {
                      float scoreA = sHits.getScoreOnEntity(c);
                      float scoreB = hit2score.get(c);
                      if (scoreA > scoreB) {
                          hit2score.put(c, scoreA);
                      }
                  }
              }
          }
      }
  
      /**
       * Search for concepts in OndexKB which contain the keyword and find genes
       * connected to them.
       *
       * @param keyword user-specified keyword
       * @return set of genes related to the keyword
       * @throws IOException
       * @throws ParseException
       */
      public HashMap<ONDEXConcept, Float> searchLucene(String keywords, Collection<ONDEXConcept> geneList, boolean includePublications) throws IOException, ParseException {
  
          Set<AttributeName> atts = graph.getMetaData().getAttributeNames();
          String[] datasources = {"PFAM", "IPRO", "UNIPROTKB", "EMBL", "KEGG", "EC", "GO", "TO", "NLM", "TAIR",
                  "ENSEMBLGENE", "PHYTOZOME", "IWGSC", "IBSC", "PGSC", "ENSEMBL"};
          // sources identified in KNETviewer
          /*
           * String[] new_datasources= { "AC", "DOI", "CHEBI", "CHEMBL", "CHEMBLASSAY",
           * "CHEMBLTARGET", "EC", "EMBL", "ENSEMBL", "GENB", "GENOSCOPE", "GO", "INTACT",
           * "IPRO", "KEGG", "MC", "NC_GE", "NC_NM", "NC_NP", "NLM", "OMIM", "PDB",
           * "PFAM", "PlnTFDB", "Poplar-JGI", "PoplarCyc", "PRINTS", "PRODOM", "PROSITE",
           * "PUBCHEM", "PubMed", "REAC", "SCOP", "SOYCYC", "TAIR", "TX", "UNIPROTKB"};
           */
          Set<String> dsAcc = new HashSet<String>(Arrays.asList(datasources));
  
          HashMap<ONDEXConcept, Float> hit2score = new HashMap<ONDEXConcept, Float>();
  
          if ("".equals(keywords) || keywords == null) {
              log.info("No keyword, skipping Lucene stage, using mapGene2Concept instead");
              if (geneList != null) {
                  for (ONDEXConcept gene : geneList) {
                      for (int conceptId : mapGene2Concepts.get(gene.getId())) {
                          ONDEXConcept concept = graph.getConcept(conceptId);
                          if (includePublications || !concept.getOfType().getId().equalsIgnoreCase("Publication")) {
                              hit2score.put(concept, 1.0f);
                          }
                      }
                  }
              }
              return hit2score;
          }
  
          // Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_36);
          Analyzer analyzer = new StandardAnalyzer();
  
          String keyword = keywords;
  
          // creates the NOT list (list of all the forbidden documents)
          String NOTQuery = createsNotList(keyword);
          String crossTypesNotQuery = "";
          ScoredHits<ONDEXConcept> NOTList = null;
          if (NOTQuery != "") {
              crossTypesNotQuery = "ConceptAttribute_AbstractHeader:(" + NOTQuery + ") OR ConceptAttribute_Abstract:("
                      + NOTQuery + ") OR Annotation:(" + NOTQuery + ") OR ConceptName:(" + NOTQuery + ") OR ConceptID:("
                      + NOTQuery + ")";
              String fieldNameNQ = getFieldName("ConceptName", null);
              // QueryParser parserNQ = new QueryParser(Version.LUCENE_36, fieldNameNQ,
              // analyzer);
              QueryParser parserNQ = new QueryParser(fieldNameNQ, analyzer);
              Query qNQ = parserNQ.parse(crossTypesNotQuery);
              NOTList = lenv.searchTopConcepts(qNQ, 2000);
          }
  
          // number of top concepts retrieved for each Lucene field
          /*
           * increased for now from 500 to 1500, until Lucene code is ported from Ondex to
           * QTLNetMiner, when we'll make changes to the QueryParser code instead.
           */
          int max_concepts = 2000/* 500 */;
  
          // search concept attributes
          for (AttributeName att : atts) {
              // Query qAtt =
              // LuceneQueryBuilder.searchConceptByConceptAttributeExact(att,
              // keyword);
              // ScoredHits<ONDEXConcept> sHits = lenv.searchTopConcepts(qAtt,
              // 100);
              // mergeHits(hit2score, sHits);
  
              String fieldName = getFieldName("ConceptAttribute", att.getId());
              // QueryParser parser = new QueryParser(Version.LUCENE_36, fieldName, analyzer);
              QueryParser parser = new QueryParser(fieldName, analyzer);
              Query qAtt = parser.parse(keyword);
              ScoredHits<ONDEXConcept> sHits = lenv.searchTopConcepts(qAtt, max_concepts);
              mergeHits(hit2score, sHits, NOTList);
  
          }
          for (String dsAc : dsAcc) {
              // search concept accessions
              // Query qAccessions =
              // LuceneQueryBuilder.searchConceptByConceptAccessionExact(keyword,
              // false, dsAcc);
              String fieldName = getFieldName("ConceptAccessions", dsAc);
              // QueryParser parser = new QueryParser(Version.LUCENE_36, fieldName, analyzer);
              QueryParser parser = new QueryParser(fieldName, analyzer);
              Query qAccessions = parser.parse(keyword);
              ScoredHits<ONDEXConcept> sHitsAcc = lenv.searchTopConcepts(qAccessions, max_concepts);
              mergeHits(hit2score, sHitsAcc, NOTList);
          }
  
          // search concept names
          // Query qNames =
          // LuceneQueryBuilder.searchConceptByConceptNameExact(keyword);
          String fieldNameCN = getFieldName("ConceptName", null);
          // QueryParser parserCN = new QueryParser(Version.LUCENE_36, fieldNameCN,
          // analyzer);
          QueryParser parserCN = new QueryParser(fieldNameCN, analyzer);
          Query qNames = parserCN.parse(keyword);
          ScoredHits<ONDEXConcept> sHitsNames = lenv.searchTopConcepts(qNames, max_concepts);
          mergeHits(hit2score, sHitsNames, NOTList);
  
          // search concept description
          // Query qDesc =
          // LuceneQueryBuilder.searchConceptByDescriptionExact(keyword);
          String fieldNameD = getFieldName("Description", null);
          // QueryParser parserD = new QueryParser(Version.LUCENE_36, fieldNameD,
          // analyzer);
          QueryParser parserD = new QueryParser(fieldNameD, analyzer);
          Query qDesc = parserD.parse(keyword);
          ScoredHits<ONDEXConcept> sHitsDesc = lenv.searchTopConcepts(qDesc, max_concepts);
          mergeHits(hit2score, sHitsDesc, NOTList);
  
          // search concept annotation
          // Query qAnno =
          // LuceneQueryBuilder.searchConceptByAnnotationExact(keyword);
          String fieldNameCA = getFieldName("Annotation", null);
          // QueryParser parserCA = new QueryParser(Version.LUCENE_36, fieldNameCA,
          // analyzer);
          QueryParser parserCA = new QueryParser(fieldNameCA, analyzer);
          Query qAnno = parserCA.parse(keyword);
          ScoredHits<ONDEXConcept> sHitsAnno = lenv.searchTopConcepts(qAnno, max_concepts);
          mergeHits(hit2score, sHitsAnno, NOTList);
  
          log.info("Query: " + qAnno.toString(fieldNameCA));
          log.info("Annotation hits: " + sHitsAnno.getOndexHits().size());
  
          return hit2score;
      }
  
      public ArrayList<ONDEXConcept> getScoredGenes(Map<ONDEXConcept, Float> hit2score) throws IOException {
          return new ArrayList<ONDEXConcept>(this.getScoredGenesMap(hit2score).keySet());
      }
  
      public SortedMap<ONDEXConcept, Double> getScoredGenesMap(Map<ONDEXConcept, Float> hit2score) throws IOException {
          scoredCandidates = new HashMap<ONDEXConcept, Double>();
          ValueComparator<ONDEXConcept> comparator = new ValueComparator<ONDEXConcept>(scoredCandidates);
          TreeMap<ONDEXConcept, Double> sortedCandidates = new TreeMap<ONDEXConcept, Double>(comparator);
  
          log.info("total hits from lucene: " + hit2score.keySet().size());
  
          // 1st step: create map of genes to concepts that contain query terms
          mapGene2HitConcept = new HashMap<Integer, Set<Integer>>();
          for (ONDEXConcept c : hit2score.keySet()) {
  
              // hit concept not connected via valid path to any gene
              if (!mapConcept2Genes.containsKey(c.getId())) {
                  continue;
              }
              Set<Integer> genes = mapConcept2Genes.get(c.getId());
              for (int geneId : genes) {
                  if (!mapGene2HitConcept.containsKey(geneId)) {
                      mapGene2HitConcept.put(geneId, new HashSet<Integer>());
                  }
  
                  mapGene2HitConcept.get(geneId).add(c.getId());
              }
          }
  
          // 2nd step: calculate a score for each candidate gene
          for (int geneId : mapGene2HitConcept.keySet()) {
  
  // weighted sum of all evidence concepts
              double weighted_evidence_sum = 0;
  
              // iterate over each evidence concept and compute a weight that is composed of
              // three components
              for (int cId : mapGene2HitConcept.get(geneId)) {
  
                  // relevance of search term to concept
                  float luceneScore = hit2score.get(graph.getConcept(cId));
  
                  // specificity of evidence to gene
                  double igf = Math.log10((double) numGenesInGenome / mapConcept2Genes.get(cId).size());
  
                  // inverse distance from gene to evidence
                  Integer path_length = mapGene2PathLength.get(geneId + "//" + cId);
  		    if(path_length==null){
  		    	log.info("WARNING: Path length is null for: "+geneId + "//" + cId);
  		    }
                  double distance = path_length==null ? 0 : (1 / path_length);
  
                  // take the mean of all three components 
                  double evidence_weight =  (igf + luceneScore + distance) / 3;
  
                  // sum of all evidence weights
                  weighted_evidence_sum += evidence_weight;
              }
  
  
              	// normalisation method 1: size of the gene knoweldge graph
              	// double normFactor = 1 / (double) mapGene2Concepts.get(geneId).size();
  		
  		// normalistion method 2: size of matching evidence concepts only (mean score)
  		//double normFactor = 1 / Math.max((double) mapGene2HitConcept.get(geneId).size(), 3.0);
  
             	
  		// No normalisation for now as it's too experimental. 
  		// This meeans better studied genes will appear top of the list
              double knetScore = /*normFactor * */ weighted_evidence_sum;
  
              scoredCandidates.put(graph.getConcept(geneId), knetScore);
          }
  
          sortedCandidates.putAll(scoredCandidates);
  
          return sortedCandidates;
      }
  
      // /**
      // * From the given set concepts (genes) here we return those associated
      // with the keyword.
      // *
      // * @param List<String> list
      // * @param String keyword
      // *
      // * @return ArrayList<ONDEXConcept> genes
      // */
      // public Set<ONDEXConcept> searchList(Set<ONDEXConcept> list, String
      // keyword){
      // Set<ONDEXConcept> relatedGenes = new HashSet<ONDEXConcept>();
      //
      // for(ONDEXConcept concept : list){
      // if(mapGene2Concepts.containsKey(concept.getId())){
      // Set<Integer> accessions = mapGene2Concepts.get(concept.getId());
      // for(Integer acc : accessions){
      // ONDEXConcept linkedConcept = graph.getConcept(acc);
      // if (OndexSearch.find(linkedConcept, keyword, false)) {
      // relatedGenes.add(concept);
      // break;
      // }
      // }
      // }
      // }
      //
      //
      // System.out.println("related genes size: "+relatedGenes.size());
      // return relatedGenes;
      // }
  
      /**
       * Did you mean function for spelling correction
       *
       * @param String keyword
       * @return list of spell corrected words
       */
      public List<String> didyoumean(String keyword) throws ParseException {
          List<String> alternatives = new ArrayList<String>();
  
          return alternatives;
      }
  
      /**
       * Searches for genes within genomic regions (QTLs)
       *
       * @param List<String> qtlsStr
       * @return Set<ONDEXConcept> concepts
       */
      public Set<ONDEXConcept> searchQTLs(List<String> qtlsStr) {
          log.info("qtlsStr: " + qtlsStr); // qtl string
          Set<ONDEXConcept> concepts = new HashSet<ONDEXConcept>();
  
          // convert List<String> qtlStr to List<QTL> qtls
          List<QTL> qtls = new ArrayList<QTL>();
          for (String qtlStr : qtlsStr) {
              qtls.add(QTL.fromString(qtlStr));
          }
  
          String chrQTL;
          int startQTL, endQTL;
          for (QTL qtl : qtls) {
              try {
                  chrQTL = qtl.getChromosome();
                  startQTL = qtl.getStart();
                  endQTL = qtl.getEnd();
                  log.info("user QTL (chr, start, end): " + chrQTL + " , " + startQTL + " , " + endQTL);
                  // swap start with stop if start larger than stop
                  if (startQTL > endQTL) {
                      int tmp = startQTL;
                      startQTL = endQTL;
                      endQTL = tmp;
                  }
                  ConceptClass ccGene = graph.getMetaData().getConceptClass("Gene");
                  AttributeName attBegin = graph.getMetaData().getAttributeName("BEGIN");
                  AttributeName attCM = graph.getMetaData().getAttributeName("cM");
                  AttributeName attChromosome = graph.getMetaData().getAttributeName("Chromosome");
                  // AttributeName attLoc = graph.getMetaData().getAttributeName("Location");
                  AttributeName attTAXID = graph.getMetaData().getAttributeName("TAXID");
                  Set<ONDEXConcept> genes = graph.getConceptsOfConceptClass(ccGene);
                  log.info("genes matched from entire network: " + genes.size());
  
                  for (ONDEXConcept c : genes) {
                      String chrGene = null;
                      int startGene = 0;
                      if (c.getAttribute(attTAXID) == null
                              || !taxID.contains(c.getAttribute(attTAXID).getValue().toString())) {
                          continue;
                      }
                      if (c.getAttribute(attChromosome) != null) {
                          chrGene = c.getAttribute(attChromosome).getValue().toString();
                      }
                      // TEMPORARY FIX, to be disabled for new .oxl species networks that have string
                      // 'Chromosome' (instead of the older integer Chromosome) & don't have a string
                      // 'Location' attribute.
                      /*
                       * if(c.getAttribute(attLoc) != null) { // if String Location exists, use that
                       * instead of integer Chromosome as client-side may use String Location in
                       * basemap. chrGene= c.getAttribute(attLoc).getValue().toString(); }
                       */
  
  					/*if (attCM != null) {
  						if (c.getAttribute(attCM) != null) {
  							startGene = (Double) c.getAttribute(attCM).getValue();
  						}
  					} else*/
                      if (c.getAttribute(attBegin) != null) {
                          startGene = /*(double)*/ ((Integer) c.getAttribute(attBegin).getValue());
                      }
                      if (chrGene != null && startGene != 0) {
                          //log.info("Gene (chr, start) found: "+ chrGene +","+ startGene);
                          if (chrQTL.equals(chrGene)) {
                              if ((startGene >= startQTL) && (startGene <= endQTL)) {
                                  concepts.add(c);
                              }
                          }
  
                      }
                  }
              } catch (Exception e) {
                  log.error("Not valid qtl", e);
              }
          }
          return concepts;
      }
  
      /**
       * Searches the knowledge base for QTL concepts that match any of the user input
       * terms
       *
       * @param keyword
       * @return list of QTL objects
       */
      public Set<QTL> findQTL(String keyword) throws ParseException {
  
          ConceptClass ccTrait = graph.getMetaData().getConceptClass("Trait");
          ConceptClass ccQTL = graph.getMetaData().getConceptClass("QTL");
          ConceptClass ccSNP = graph.getMetaData().getConceptClass("SNP");
  
          // no Trait-QTL relations found
          if (ccTrait == null && (ccQTL == null || ccSNP == null)) {
              return new HashSet<QTL>();
          }
  
          AttributeName attBegin = graph.getMetaData().getAttributeName("BEGIN");
          AttributeName attEnd = graph.getMetaData().getAttributeName("END");
          AttributeName attPvalue = graph.getMetaData().getAttributeName("PVALUE");
          AttributeName attChromosome = graph.getMetaData().getAttributeName("Chromosome");
          AttributeName attTrait = graph.getMetaData().getAttributeName("Trait");
          AttributeName attTaxID = graph.getMetaData().getAttributeName("TAXID");
  
          Set<QTL> results = new HashSet<QTL>();
  
          log.debug("Looking for QTLs...");
          // If there is not traits but there is QTLs then we return all the QTLs
          if (ccTrait == null) {
              log.info("No Traits found: all QTLS will be shown...");
              // results = graph.getConceptsOfConceptClass(ccQTL);
              for (ONDEXConcept q : graph.getConceptsOfConceptClass(ccQTL)) {
                  String type = q.getOfType().getId();
                  String chrName = q.getAttribute(attChromosome).getValue().toString();
                  Integer start = (Integer) q.getAttribute(attBegin).getValue();
                  Integer end = (Integer) q.getAttribute(attEnd).getValue();
                  String label = q.getConceptName().getName();
                  String trait = "";
                  if (attTrait != null && q.getAttribute(attTrait) != null) {
                      trait = q.getAttribute(attTrait).getValue().toString();
                  }
                  String tax_id = "";
                  if (attTaxID != null && q.getAttribute(attTaxID) != null) {
                      tax_id = q.getAttribute(attTaxID).getValue().toString();
                      // log.info("findQTL(): ccTrait=null; concept Type: "+ type +",
                      // chrName: "+ chrName +", tax_id= "+ tax_id);
                  }
                  results.add(new QTL(chrName, type, start, end, label, "", 1.0f, trait, tax_id));
              }
          } else {
              // be careful with the choice of analyzer: ConceptClasses are not
              // indexed in lowercase letters which let the StandardAnalyzer crash
              // Analyzer analyzerSt = new StandardAnalyzer(Version.LUCENE_36);
              Analyzer analyzerSt = new StandardAnalyzer();
              // Analyzer analyzerWS = new WhitespaceAnalyzer(Version.LUCENE_36);
              Analyzer analyzerWS = new WhitespaceAnalyzer();
  
              String fieldCC = getFieldName("ConceptClass", null);
              // QueryParser parserCC = new QueryParser(Version.LUCENE_36, fieldCC,
              // analyzerWS);
              QueryParser parserCC = new QueryParser(fieldCC, analyzerWS);
              Query cC = parserCC.parse("Trait");
  
              String fieldCN = getFieldName("ConceptName", null);
              // QueryParser parserCN = new QueryParser(Version.LUCENE_36, fieldCN,
              // analyzerSt);
              QueryParser parserCN = new QueryParser(fieldCN, analyzerSt);
              Query cN = parserCN.parse(keyword);
  
              /*
               * BooleanQuery finalQuery = new BooleanQuery(); finalQuery.add(cC, Occur.MUST);
               * finalQuery.add(cN, Occur.MUST);
               */
              BooleanQuery finalQuery = new BooleanQuery.Builder().add(cC, BooleanClause.Occur.MUST)
                      .add(cN, BooleanClause.Occur.MUST).build();
              log.info("QTL search query: " + finalQuery.toString());
  
              ScoredHits<ONDEXConcept> hits = lenv.searchTopConcepts(finalQuery, 100);
  
              for (ONDEXConcept c : hits.getOndexHits()) {
                  if (c instanceof LuceneConcept) {
                      c = ((LuceneConcept) c).getParent();
                  }
                  Set<ONDEXRelation> rels = graph.getRelationsOfConcept(c);
                  for (ONDEXRelation r : rels) {
                      // get QTL concept
                      if (r.getFromConcept().getOfType().equals(ccQTL) || r.getToConcept().getOfType().equals(ccQTL)
                              || r.getFromConcept().getOfType().equals(ccSNP)
                              || r.getToConcept().getOfType().equals(ccSNP)) {
                          // QTL-->Trait or SNP-->Trait
                          // log.info("QTL-->Trait or SNP-->Trait");
                          ONDEXConcept conQTL = r.getFromConcept();
                          // results.add(conQTL);
                          if (conQTL.getAttribute(attChromosome) != null && conQTL.getAttribute(attBegin) != null
                                  && conQTL.getAttribute(attEnd) != null) {
                              String type = conQTL.getOfType().getId();
                              String chrName = conQTL.getAttribute(attChromosome).getValue().toString();
                              Integer start = (Integer) conQTL.getAttribute(attBegin).getValue();
                              Integer end = (Integer) conQTL.getAttribute(attEnd).getValue();
                              String label = conQTL.getConceptName().getName();
                              Float pValue = 1.0f;
                              if (attPvalue != null && r.getAttribute(attPvalue) != null) {
                                  pValue = (Float) r.getAttribute(attPvalue).getValue();
                              }
                              String trait = c.getConceptName().getName();
                              String tax_id = "";
                              // log.info("findQTL(): conQTL.getAttribute(attTaxID): "+
                              // conQTL.getAttribute(attTaxID) +", value= "+
                              // conQTL.getAttribute(attTaxID).getValue().toString());
                              if (attTaxID != null && conQTL.getAttribute(attTaxID) != null) {
                                  tax_id = conQTL.getAttribute(attTaxID).getValue().toString();
                                  // log.info("findQTL(): conQTL Type: "+ type +", chrName: "+ chrName
                                  // +", tax_id= "+ tax_id);
                              }
                              results.add(new QTL(chrName, type, start, end, label, "", pValue, trait, tax_id));
                          }
                      }
                  }
              }
          }
          return results;
      }
  
      /**
       * Semantic Motif Search for list of genes
       *
       * @param accessions
       * @param regex      trait-related
       * @return OndexGraph containing the gene network
       */
      public ONDEXGraph getGenes(Integer[] ids, String regex) {
          log.debug("get genes function " + ids.length);
          AttributeName attTAXID = graph.getMetaData().getAttributeName("TAXID");
  
          Set<ONDEXConcept> seed = new HashSet<ONDEXConcept>();
  
          for (int id : ids) {
              ONDEXConcept c = graph.getConcept(id);
              if (c.getAttribute(attTAXID) != null && taxID.contains(c.getAttribute(attTAXID).getValue().toString())) {
                  seed.add(c);
              }
          }
          log.debug("Now we will call findSemanticMotifs!");
          ONDEXGraph subGraph = findSemanticMotifs(seed, regex);
          return subGraph;
      }
  
      /**
       * Searches for ONDEXConcepts with the given accessions in the OndexGraph.
       *
       * @param List<String> accessions
       * @return Set<ONDEXConcept>
       */
      public Set<ONDEXConcept> searchGenes(List<String> accessions) {
  
          if (accessions.size() > 0) {
              AttributeName attTAXID = graph.getMetaData().getAttributeName("TAXID");
              ConceptClass ccGene = graph.getMetaData().getConceptClass("Gene");
              Set<ONDEXConcept> seed = graph.getConceptsOfConceptClass(ccGene);
              Set<ONDEXConcept> hits = new HashSet<ONDEXConcept>();
  
              // create one regex string for efficient search
              String query = "";
              for (String acc : accessions) {
                  query += acc + "|";
              }
              query = query.substring(0, query.length() - 1);
              for (ONDEXConcept gene : seed) {
                  if (gene.getAttribute(attTAXID) != null
                          && taxID.contains(gene.getAttribute(attTAXID).getValue().toString())) {
  
                      // search gene accessions, names, attributes
                      if (OndexSearch.find(gene, query)) {
                          hits.add(gene);
                      }
                  }
              }
              return hits;
          } else {
              return null;
          }
      }
  
      /**
       * Searches genes related to an evidence, finds semantic motifs and shows the
       * path between them
       *
       * @param evidenceOndexId
       * @return subGraph
       */
      public ONDEXGraph evidencePath(Integer evidenceOndexId, Set<ONDEXConcept> genes) {
          log.info("Method evidencePath - evidenceOndexId: " + evidenceOndexId.toString());
          // Searches genes related to the evidenceID. If user genes provided, only include those.
          Set<ONDEXConcept> relatedONDEXConcepts = new HashSet<ONDEXConcept>();
          for (Integer rg : mapConcept2Genes.get(evidenceOndexId)) {
              ONDEXConcept gene = graph.getConcept(rg);
              if (genes==null || genes.isEmpty() || genes.contains(gene)) {
                  relatedONDEXConcepts.add(gene);
              }
          }
  
          // the results give us a map of every starting concept to every valid
          // path
          Map<ONDEXConcept, List<EvidencePathNode>> results = gt.traverseGraph(graph, relatedONDEXConcepts, null);
  
          // create new graph to return
          ONDEXGraph subGraph = new MemoryONDEXGraph("evidencePathGraph");
          ONDEXGraphCloner graphCloner = new ONDEXGraphCloner(graph, subGraph);
          ONDEXGraphRegistry.graphs.put(subGraph.getSID(), subGraph);
          // Highlights the right path and hides the path that doesn't leads to
          // the evidence
          for (List<EvidencePathNode> paths : results.values()) {
              for (EvidencePathNode path : paths) {
                  // search last concept of semantic motif for keyword
                  int indexLastCon = path.getConceptsInPositionOrder().size() - 1;
                  ONDEXConcept lastCon = (ONDEXConcept) path.getConceptsInPositionOrder().get(indexLastCon);
                  if (lastCon.getId() == evidenceOndexId) {
                      highlightPath(path, graphCloner);
                  } else {
                      //hidePath(path,graphCloner);
                  }
              }
          }
          ONDEXGraphRegistry.graphs.remove(subGraph.getSID());
  
          return subGraph;
      }
  
      /**
       * Tests if there is a match of this query within the search string
       *
       * @param p      the pattern (if null then literal match .contains is used)
       * @param query  the query to match
       * @param target the target to match to
       * @return is there a match
       */
      private boolean isMatching(Pattern p, String target) {
          return p.matcher(target).find(0);
      }
  
      /**
       * Converts a keyword into a list of words
       *
       * @param keyword
       * @return null or the list of words
       */
      private Set<String> parseKeywordIntoSetOfWords(String keyword) {
          Set<String> result = new HashSet<String>();
          String key = keyword.replace("(", " ");
          key = key.replace(")", " ");
          key = key.replace("AND", " ");
          key = key.replace("OR", " ");
          key = key.replace("NOT", " ");
          key = key.replaceAll("\\s+", " ").trim();
          String builtK = "";
          for (String k : key.split(" ")) {
              if (k.startsWith("\"")) {
                  if (k.endsWith("\"")) {
                      result.add(k.substring(1, k.length() - 2));
                  } else {
                      builtK = k.substring(1);
                  }
              } else if (k.endsWith("\"")) {
                  builtK += " " + k.substring(0, k.length() - 1);
                  result.add(builtK);
                  builtK = "";
              } else {
                  if (builtK != "") {
                      builtK += " " + k;
                  } else {
                      result.add(k);
                  }
              }
  //				System.out.println("subkeyworkd: "+k);
          }
          if (builtK != "") {
              result.add(builtK);
          }
          log.info("keys: " + result);
          return result;
      }
  
      /**
       * Searches different fields of a concept for a query or pattern
       * and highlights them
       *
       * @param concept
       * @param p
       * @param search
       * @return true if one of the concept fields contains the query
       */
      private boolean highlight(ONDEXConcept concept, Map<String, String> keywordColourMap) {
  //			System.out.println("Original keyword: "+regex);
          boolean found = false;
  
          // Order the keywords by length to prevent interference by shorter matches that are substrings of longer ones.
          String[] orderedKeywords = keywordColourMap.keySet().toArray(new String[0]);
          Arrays.sort(orderedKeywords, new Comparator<String>() {
              public int compare(String o1, String o2) {
                  if (o1.length() == o2.length()) {
                      return o1.compareTo(o2); // Same length? Compare alphabetically
                  } else {
                      return o1.length() - o2.length(); // Otherwise put the shorter one first
                  }
              }
          });
  
          // First pass: wrap matches in ____. Prevents substring interference
          String marker = "____";
          for (String key : orderedKeywords) {
              Pattern p = Pattern.compile(key, Pattern.CASE_INSENSITIVE);
              String highlighter = marker + key + marker;
  
              //Searchs in annotations
              String anno = concept.getAnnotation();
              if (this.isMatching(p, anno)) {
                  found = true;
                  // search and replace all matching expressions
                  String newAnno = p.matcher(anno).replaceAll(highlighter);
                  concept.setAnnotation(newAnno);
              }
  
              //Searchs in descriptions
  