blob: cf76c77f2ba933be8329455a631c4764fddf203e [file] [log] [blame]
import re
import itertools
from dataclasses import dataclass
from collections import defaultdict
from typing import Optional
import jira2markdown
from jira2markdown.elements import MarkupElements
from jira2markdown.markup.lists import UnorderedList, OrderedList
from jira2markdown.markup.text_effects import BlockQuote, Quote, Monospaced
from jira2markdown.markup.text_breaks import Ruler
from markup.lists import UnorderedTweakedList, OrderedTweakedList
from markup.text_effects import EscapeHtmlTag, TweakedBlockQuote, TweakedQuote, TweakedMonospaced
from markup.text_breaks import LongRuler
@dataclass
class Attachment(object):
filename: str
created: str
content: str
mime_type: str
def extract_summary(o: dict) -> str:
return o.get("fields").get("summary", "")
def extract_description(o: dict) -> str:
description = o.get("fields").get("description", "")
return description if description else ""
def extract_status(o: dict) -> str:
status = o.get("fields").get("status")
return status.get("name", "") if status else ""
def extract_issue_type(o: dict) -> str:
issuetype = o.get("fields").get("issuetype")
return issuetype.get("name", "") if issuetype else ""
def extract_environment(o: dict) -> str:
environment = o.get("fields").get("environment", "")
return environment if environment else ""
def extract_resolution(o: dict) -> Optional[str]:
resolution = o.get("fields").get("resolution")
if resolution:
return resolution.get("name")
return None
def extract_priority(o: dict) -> Optional[str]:
priority = o.get("fields").get("priority")
if priority:
return priority.get("name")
return None
def extract_vote_count(o: dict) -> Optional[int]:
votes = o.get("fields").get("votes")
if votes:
vote_count = votes.get("votes")
if vote_count > 0:
return vote_count
return None
def extract_reporter(o: dict) -> tuple[str, str]:
reporter = o.get("fields").get("reporter")
name = reporter.get("name", "") if reporter else ""
disp_name = reporter.get("displayName", "") if reporter else ""
return (name, disp_name)
def extract_assignee(o: dict) -> tuple[str, str]:
assignee = o.get("fields").get("assignee")
name = assignee.get("name", "") if assignee else ""
disp_name = assignee.get("displayName", "") if assignee else ""
return (name, disp_name)
def extract_parent_key(o: dict) -> Optional[str]:
parent = o["fields"].get("parent")
if parent:
key = parent["key"]
if key:
return key
return None
def extract_created(o: dict) -> str:
return o.get("fields").get("created", "")
def extract_updated(o: dict) -> str:
return o.get("fields").get("updated", "")
def extract_resolutiondate(o: dict) -> str:
return o.get("fields").get("resolutiondate", "")
def extract_fixversions(o: dict) -> list[str]:
return [x.get("name", "") for x in o.get("fields").get("fixVersions", [])]
def extract_versions(o: dict) -> list[str]:
return [x.get("name", "") for x in o.get("fields").get("versions", [])]
def extract_components(o: dict) -> list[str]:
return [x.get("name", "") for x in o.get("fields").get("components", [])]
def extract_labels(o: dict) -> list[str]:
return o.get("fields").get("labels", [])
def extract_attachments(o: dict) -> list[tuple[str, int]]:
attachments = o.get("fields").get("attachment")
if not attachments:
return []
files = {}
counts = defaultdict(int)
for a in attachments:
filename = a.get("filename")
created = a.get("created")
content = a.get("content")
mime_type = a.get("mimeType")
if not (filename and created and content and mime_type):
continue
if filename not in files or created > files[filename].created:
files[filename] = Attachment(filename=filename, created=created, content=content, mime_type=mime_type)
counts[filename] += 1
result = []
for name in files.keys():
result.append((name, counts[name]))
return result
def extract_issue_links(o: dict) -> list[str]:
issue_links = o.get("fields").get("issuelinks", [])
if not issue_links:
return []
res = []
for link in issue_links:
key = link.get("outwardIssue", {}).get("key")
if key:
res.append(key)
key = link.get("inwardIssue", {}).get("key")
if key:
res.append(key)
return res
def extract_subtasks(o: dict) -> list[str]:
return [x.get("key", "") for x in o.get("fields").get("subtasks", [])]
def extract_comments(o: dict) -> list[tuple[str, str, str, str, str, str]]:
comments = o.get("fields").get("comment", {}).get("comments", [])
if not comments:
return []
res = []
for c in comments:
author = c.get("author")
name = author.get("name", "") if author else ""
disp_name = author.get("displayName", "") if author else ""
body = c.get("body", "")
created = c.get("created", "")
updated = c.get("updated", "")
comment_id = c.get("id", "")
res.append((name, disp_name, body, created, updated, comment_id))
return res
def extract_pull_requests(o: dict) -> list[str]:
worklogs = o.get("fields").get("worklog", {}).get("worklogs", [])
if not worklogs:
return []
res = []
for wl in worklogs:
if wl.get("author").get("name", "") != "githubbot":
continue
comment: str = wl.get("comment", "")
if not comment:
continue
if "opened a new pull request" not in comment and not "opened a pull request" in comment:
continue
comment = comment.replace('\n', ' ')
# detect pull request url
matches = re.match(r".*(https://github\.com/apache/lucene/pull/\d+)", comment)
if matches:
res.append(matches.group(1))
# detect pull request url in old lucene-solr repo
matches = re.match(r".*(https://github\.com/apache/lucene-solr/pull/\d+)", comment)
if matches:
res.append(matches.group(1))
return res
REGEX_EMBEDDED_IMAGE = r"!([^!\n]+)!"
# space character + Jira emoji + space character
JIRA_EMOJI_TO_UNICODE = {
"(?<=\s)\(y\)((?=$)|(?=\s))": "\U0001F44D",
"(?<=\s)\(n\)((?=$)|(?=\s))": "\U0001F44E",
"(?<=\s)\(i\)((?=$)|(?=\s))": "\U0001F6C8",
"(?<=\s)\(/\)((?=$)|(?=\s))": "\u2714",
"(?<=\s)\(x\)((?=$)|(?=\s))": "\u274C",
"(?<=\s)\(\!\)((?=$)|(?=\s))": "\u26A0",
"(?<=\s)\(\+\)((?=$)|(?=\s))": "\u002B",
"(?<=\s)\(\-\)((?=$)|(?=\s))": "\u2212",
"(?<=\s)\(\?\)((?=$)|(?=\s))": "\u003F",
"(?<=\s)\(on\)((?=$)|(?=\s))": "\U0001F4A1",
"(?<=\s)\(off\)((?=$)|(?=\s))": "\U0001F4A1",
"(?<=\s)\(\*\)((?=$)|(?=\s))": "\u2B50",
"(?<=\s)\(\*r\)((?=$)|(?=\s))": "\u2B50",
"(?<=\s)\(\*g\)((?=$)|(?=\s))": "\u2B50",
"(?<=\s)\(\*b\)((?=$)|(?=\s))": "\u2B50",
"(?<=\s)\(flag\)((?=$)|(?=\s))": "\U0001F3F4",
"(?<=\s)\(flagoff\)((?=$)|(?=\s))": "\U0001F3F3"
}
REGEX_CRLF = re.compile(r"\r\n")
REGEX_JIRA_KEY = re.compile(r"[^/]LUCENE-\d+")
REGEX_MENTION_ATMARK = re.compile(r"(^@[\w\s\.@_-]+?)|((?<=[\s\({\[\"',.])@[\w\s\.@_-]+?)($|(?=[\s\)}\]\"'\?!,,;:\..]))") # this regex may capture only "@" + "<username>" mentions
REGEX_MENION_TILDE = re.compile(r"(^\[~[\w\s\.@_-]+?\])|((?<=[\s\({\[\"',.])\[~[\w\s\.@_-]+?\])($|(?=[\s\)|\]\"'\?!,,;:\..]))") # this regex may capture only "[~" + "<username>" + "]" mentions
REGEX_LINK = re.compile(r"\[([^\]]+)\]\(([^\)]+)\)")
REGEX_GITHUB_ISSUE_LINK = re.compile(r"(\s)(#\d+)(\s)")
def extract_embedded_image_files(text: str, image_files: list[str]) -> set[str]:
"""Extract embedded image files in the given text.
https://jira.atlassian.com/secure/WikiRendererHelpAction.jspa?section=images
"""
# capture candidates for embedded images
candidates = re.findall(REGEX_EMBEDDED_IMAGE, text)
embedded_image_files = set([])
for x in candidates:
if x in image_files:
# !xyz.png!
embedded_image_files.add(x)
elif any(map(lambda s: x.startswith(s + "|"), image_files)):
# !xyz.png|styles!
embedded_image_files.add(x.split("|", 1)[0])
return embedded_image_files
def convert_text(text: str, att_replace_map: dict[str, str] = {}, account_map: dict[str, str] = {}, jira_users: dict[str, str] = {}) -> str:
"""Convert Jira markup to Markdown
"""
def repl_att(m: re.Match):
res = m.group(0)
for src, repl in att_replace_map.items():
if m.group(2) == src:
res = f"[{m.group(1)}]({repl})"
return res
def escape_gh_issue_link(m: re.Match):
# escape #NN by backticks to prevent creating an unintentional issue link
res = f"{m.group(1)}`{m.group(2)}`{m.group(3)}"
return res
text = re.sub(REGEX_CRLF, "\n", text) # jira2markup does not support carriage return (?)
# convert Jira special emojis into corresponding or similar Unicode characters
for emoji, unicode in JIRA_EMOJI_TO_UNICODE.items():
text = re.sub(emoji, unicode, text)
# convert @ mentions
mentions = re.findall(REGEX_MENTION_ATMARK, text)
if mentions:
mentions = set(filter(lambda x: x != '', itertools.chain.from_iterable(mentions)))
for m in mentions:
jira_id = m[1:]
disp_name = jira_users.get(jira_id)
gh_m = account_map.get(jira_id)
# replace Jira name with GitHub account or Jira display name if it is available, othewise show Jira name with `` to avoid unintentional mentions
mention = lambda: f"@{gh_m}" if gh_m else disp_name if disp_name else f"`@{jira_id}`"
text = text.replace(m, mention())
# convert ~ mentions
mentions = re.findall(REGEX_MENION_TILDE, text)
if mentions:
mentions = set(filter(lambda x: x != '', itertools.chain.from_iterable(mentions)))
for m in mentions:
jira_id = m[2:-1]
disp_name = jira_users.get(jira_id)
gh_m = account_map.get(jira_id)
# replace Jira name with GitHub account or Jira display name if it is available, othewise show Jira name with ``
mention = lambda: f"@{gh_m}" if gh_m else disp_name if disp_name else f"`@{jira_id}`"
text = text.replace(m, mention())
# escape tilde to avoid unintentional strike-throughs in Markdown
text = text.replace("~", "\~")
# convert Jira markup into Markdown with customization
elements = MarkupElements()
elements.replace(UnorderedList, UnorderedTweakedList)
elements.replace(OrderedList, OrderedTweakedList)
elements.replace(BlockQuote, TweakedBlockQuote)
elements.replace(Quote, TweakedQuote)
elements.replace(Monospaced, TweakedMonospaced)
elements.insert_after(Ruler, LongRuler)
elements.append(EscapeHtmlTag)
text = jira2markdown.convert(text, elements=elements)
# convert links to attachments
text = re.sub(REGEX_LINK, repl_att, text)
# escape github style cross-issue link (#NN)
text = re.sub(REGEX_GITHUB_ISSUE_LINK, escape_gh_issue_link, text)
return text
def embed_gh_issue_link(text: str, issue_id_map: dict[str, int], gh_number_self: int) -> str:
"""Embed GitHub issue number
"""
def repl_simple(m: re.Match):
res = m.group(0)
gh_number = issue_id_map.get(m.group(2))
if gh_number and gh_number != gh_number_self:
res = f"{m.group(1)}#{gh_number}{m.group(3)}"
return res
def repl_paren(m: re.Match):
res = m.group(0)
gh_number = issue_id_map.get(m.group(2))
if gh_number and gh_number != gh_number_self:
res = f"{m.group(1)}#{gh_number}{m.group(3)}"
return res
def repl_bracket(m: re.Match):
res = m.group(0)
gh_number = issue_id_map.get(m.group(2))
if gh_number and gh_number != gh_number_self:
res = f"#{gh_number}"
return res
def repl_md_link(m: re.Match):
res = m.group(0)
gh_number = issue_id_map.get(m.group(1))
if gh_number and gh_number != gh_number_self:
res = f"#{gh_number}"
# print(res)
return res
text = re.sub(r"(\s)(LUCENE-\d+)([\s,;:\?\!\.])", repl_simple, text)
text = re.sub(r"(^)(LUCENE-\d+)([\s,;:\?\!\.])", repl_simple, text)
text = re.sub(r"(\()(LUCENE-\d+)(\))", repl_paren, text)
text = re.sub(r"(\[)(LUCENE-\d+)(\])(?!\()", repl_bracket, text)
text = re.sub(r"\[(LUCENE-\d+)\]\(https?[^\)]+LUCENE-\d+\)", repl_md_link, text)
return text
ASF_JIRA_BASE = "https://issues.apache.org/jira/browse/"
def create_issue_links_outside_projects(text: str) -> str:
"""Create links to outside ASF projects.
"""
def repl_simple(m: re.Match):
prj = m.group(2).split("-")[0]
if prj not in ALL_JIRA_PROJECTS:
return m.group(0)
jira_link = ASF_JIRA_BASE + m.group(2)
return f"{m.group(1)}[{m.group(2)}]({jira_link}){m.group(3)}"
def repl_paren(m: re.Match):
prj = m.group(2).split("-")[0]
if prj not in ALL_JIRA_PROJECTS:
return m.group(0)
jira_link = ASF_JIRA_BASE + m.group(2)
return f"{m.group(1)}[{m.group(2)}]({jira_link}){m.group(3)}"
def repl_bracket(m: re.Match):
prj = m.group(2).split("-")[0]
if prj not in ALL_JIRA_PROJECTS:
return m.group(0)
jira_link = ASF_JIRA_BASE + m.group(2)
return jira_link
text = re.sub(r"(\s)([A-Z0-9]{2,20}-\d+)([\s,;:\?\!\.])", repl_simple, text)
text = re.sub(r"(^)([A-Z0-9]{2,20}-\d+)([\s,;:\?\!\.])", repl_simple, text)
text = re.sub(r"(\()([A-Z0-9]{2,20}-\d+)(\))", repl_paren, text)
text = re.sub(r"(\[)([A-Z0-9]{2,20}-\d+)(\])(?!\()", repl_bracket, text)
return text
ALL_JIRA_PROJECTS = set([
"AAR",
"ABDERA",
"ACCUMULO",
"ACE",
"ACL",
"AMQ",
"AMQNET",
"APLO",
"ARTEMIS",
"AMQCPP",
"AMQCLI",
"OPENWIRE",
"BLAZE",
"ADDR",
"AGILA",
"AIRAVATA",
"ALOIS",
"ARMI",
"AMATERASU",
"AMBARI",
"AMBER",
"ANAKIA",
"AGE2",
"AGEOLD",
"ANY23",
"APEXCORE",
"APEXMALHAR",
"ARROW",
"ASTERIXDB",
"AVRO",
"AWF",
"BLUEMARLIN",
"BLUR",
"CHAINSAW",
"CMDA",
"COMMONSSITE",
"COMMONSRDF",
"TESTING",
"CONCERTED",
"CORAL",
"CB",
"CRAIL",
"CURATOR",
"DATALAB",
"DATASKETCHES",
"DIRECTMEMORY",
"DORIS",
"DRILL",
"DUBBO",
"ECHARTS",
"EVENTMESH",
"FINERACT",
"FLEX",
"FREEMARKER",
"GEARPUMP",
"GOBBLIN",
"GORA",
"HAWQ",
"HELIX",
"HOP",
"HORN",
"HUDI",
"INLONG",
"IOTDB",
"JENA",
"KNOX",
"LENS",
"LIMINAL",
"LINKIS",
"CLOWNFISH",
"MADLIB",
"MARVIN",
"MASFRES",
"METAMODEL",
"MXNET",
"NEMO",
"NETBEANSINFRA",
"NIFI",
"MINIFI",
"MINIFICPP",
"NUTTX",
"OLTU",
"ONAMI",
"CLIMATE",
"OPENAZ",
"HDDS",
"PEGASUS",
"PETRI",
"PINOT",
"PLC4X",
"QPIDIT",
"QUICKSTEP",
"RAT",
"RIPPLE",
"ROCKETMQ",
"ROL",
"S4",
"SDAP",
"SCIMPLE",
"SEDONA",
"SCB",
"STORM",
"SUBMARINE",
"TAVERNA",
"TENTACLES",
"TEZ",
"MTOMCAT",
"TRAFODION",
"TRAINING",
"TWILL",
"UNOMI",
"WAYANG",
"WHIRR",
"WHISKER",
"YUNIKORN",
"ZIPKIN",
"APISIX",
"MRM",
"ARIA",
"ARIES",
"ASYNCWEB",
"ATLAS",
"ATTIC",
"AURORA",
"AVALON",
"AVNSHARP",
"RUNTIME",
"STUDIO",
"CENTRAL",
"PLANET",
"TOOLS",
"PNIX",
"AXIOM",
"AXIS",
"AXISCPP",
"WSIF",
"AXIS2",
"TRANSPORTS",
"AXIS2C",
"BAHIR",
"BATCHEE",
"BATIK",
"BEAM",
"BEEHIVE",
"BIGTOP",
"BLUESKY",
"BOOKKEEPER",
"TM",
"BROOKLYN",
"BUILDR",
"BVAL",
"STDCXX",
"CACTUS",
"CALCITE",
"CAMEL",
"CARBONDATA",
"CASSANDRA",
"CAY",
"CELIX",
"CS",
"CMIS",
"CHUKWA",
"CLEREZZA",
"CLK",
"CLKE",
"CLOUDSTACK",
"COCOON",
"COCOON3",
"ATTRIBUTES",
"BCEL",
"BEANUTILS",
"BETWIXT",
"BSF",
"CHAIN",
"CLI",
"CODEC",
"COLLECTIONS",
"COMPRESS",
"CONFIGURATION",
"CRYPTO",
"CSV",
"DAEMON",
"DBCP",
"DBUTILS",
"DIGESTER",
"DISCOVERY",
"DORMANT",
"EL",
"EMAIL",
"EXEC",
"FEEDPARSER",
"FILEUPLOAD",
"FUNCTOR",
"GEOMETRY",
"IMAGING",
"IO",
"JCI",
"JCS",
"JELLY",
"JEXL",
"JXPATH",
"LANG",
"LAUNCHER",
"LOGGING",
"MATH",
"MODELER",
"NET",
"NUMBERS",
"OGNL",
"POOL",
"PRIMITIVES",
"PROXY",
"RESOURCES",
"RNG",
"SANDBOX",
"SANSELAN",
"SCXML",
"STATISTICS",
"TEXT",
"TRANSACTION",
"VALIDATOR",
"VFS",
"WEAVER",
"COMDEV",
"CONTINUUM",
"COR",
"COTTON",
"COUCHDB",
"CRUNCH",
"CTAKES",
"CUSTOS",
"CXF",
"DOSGI",
"CXFXJC",
"FEDIZ",
"DAFFODIL",
"DATAFU",
"DAYTRADER",
"DDLUTILS",
"DTACLOUD",
"DELTASPIKE",
"DEPOT",
"DERBY",
"DMAP",
"DIR",
"DIRSERVER",
"DIRAPI",
"DIRGROOVY",
"DIRKRB",
"DIRNAMING",
"DIRSHARED",
"DIRSTUDIO",
"DL",
"DI",
"DBF",
"DROIDS",
"DVSL",
"EAGLE",
"EASYANT",
"ECS",
"EDGENT",
"EMPIREDB",
"ESME",
"ESCIMO",
"ETCH",
"EWS",
"EXLBR",
"FORTRESS",
"FALCON",
"FELIX",
"FINCN",
"FLAGON",
"FLINK",
"FLUME",
"FOP",
"FOR",
"FC",
"FTPSERVER",
"GBUILD",
"GEODE",
"GERONIMO",
"GERONIMODEVTOOLS",
"GIRAPH",
"GOSSIP",
"GRFT",
"GRIFFIN",
"GROOVY",
"GSHELL",
"GUACAMOLE",
"GUMP",
"HADOOP",
"HDT",
"HDFS",
"MAPREDUCE",
"YARN",
"HAMA",
"HARMONY",
"HBASE",
"HCATALOG",
"HERALDRY",
"HISE",
"HIVE",
"HIVEMALL",
"HIVEMIND",
"HTRACE",
"HTTPASYNC",
"HTTPCLIENT",
"HTTPCORE",
"IBATISNET",
"IBATIS",
"RBATIS",
"IGNITE",
"IMPALA",
"IMPERIUS",
"INCUBATOR",
"INFRATEST3",
"INFRATEST987",
"INFRACLOUD1",
"INFRA",
"TEST6",
"IOTA",
"ISIS",
"IVY",
"IVYDE",
"JCR",
"JCRVLT",
"JCRBENCH",
"JCRCL",
"JCRSERVLET",
"JCRTCK",
"JCRRMI",
"OAK",
"OCM",
"JCRSITE",
"HUPA",
"IMAP",
"JDKIM",
"JSIEVE",
"JSPF",
"MAILBOX",
"MAILET",
"MIME4J",
"MPT",
"POSTAGE",
"PROTOCOLS",
"JAMES",
"JAXME",
"JCLOUDS",
"JDO",
"JS1",
"JS2",
"JOHNZON",
"JOSHUA",
"JSEC",
"JSPWIKI",
"JUDDI",
"JUNEAU",
"KAFKA",
"KALUMET",
"KAND",
"KARAF",
"KATO",
"KI",
"KITTY",
"KUDU",
"KYLIN",
"LABS",
"HTTPDRAFT",
"LEGAL",
"LIBCLOUD",
"LIVY",
"LOGCXX",
"LOG4J2",
"LOG4NET",
"LOG4PHP",
"LOKAHI",
"LUCENENET",
"LCN4C",
"LUCY",
"MAHOUT",
"CONNECTORS",
"MARMOTTA",
"MNG",
"MACR",
"MANT",
"MANTTASKS",
"MANTRUN",
"ARCHETYPE",
"MARCHETYPES",
"MARTIFACT",
"MASSEMBLY",
"MBUILDCACHE",
"MCHANGELOG",
"MCHANGES",
"MCHECKSTYLE",
"MCLEAN",
"MCOMPILER",
"MDEP",
"MDEPLOY",
"MDOAP",
"MDOCCK",
"DOXIA",
"DOXIASITETOOLS",
"DOXIATOOLS",
"MEAR",
"MECLIPSE",
"MEJB",
"MENFORCER",
"MGPG",
"MPH",
"MINDEXER",
"MINSTALL",
"MINVOKER",
"MJAR",
"MJARSIGNER",
"MJAVADOC",
"MJDEPRSCAN",
"MJDEPS",
"MJLINK",
"MJMOD",
"JXR",
"MLINKCHECK",
"MPATCH",
"MPDF",
"MPLUGINTESTING",
"MPLUGIN",
"MPMD",
"MPOM",
"MPIR",
"MNGSITE",
"MRAR",
"MRELEASE",
"MRRESOURCES",
"MREPOSITORY",
"MRESOLVER",
"MRESOURCES",
"SCM",
"MSCMPUB",
"MSCRIPTING",
"MSHADE",
"MSHARED",
"MSITE",
"MSKINS",
"MSOURCES",
"MSTAGE",
"SUREFIRE",
"MTOOLCHAINS",
"MVERIFIER",
"WAGON",
"MWAR",
"MWRAPPER",
"MAVIBOT",
"MEECROWAVE",
"MESOS",
"METRON",
"MILAGRO",
"DIRMINA",
"SSHD",
"MIRAE",
"MNEMONIC",
"MODPYTHON",
"MRQL",
"MRUNIT",
"MUSE",
"MXNETTEST",
"ADFFACES",
"EXTCDI",
"MFCOMMONS",
"MYFACES",
"EXTSCRIPT",
"EXTVAL",
"MFHTML5",
"ORCHESTRA",
"PORTLETBRIDGE",
"MYFACESTEST",
"TOBAGO",
"TOMAHAWK",
"TRINIDAD",
"MYNEWT",
"MYNEWTDOC",
"MYRIAD",
"NEETHI",
"NETBEANS",
"NIFIREG",
"NIFILIBS",
"NLPCRAFT",
"NPANDAY",
"NUTCH",
"NUVEM",
"ODE",
"JACOB",
"OWC",
"ODFTOOLKIT",
"OFBIZ",
"OJB",
"OLINGO",
"OLIO",
"OODT",
"OOZIE",
"ORP",
"OPENEJB",
"OEP",
"OPENJPA",
"OPENMEETINGS",
"OPENNLP",
"OPENOFFICE",
"OWB",
"ORC",
"PARQUET",
"PDFBOX",
"PHOENIX",
"OMID",
"TEPHRA",
"PHOTARK",
"PIG",
"PIRK",
"PIVOT",
"PLUTO",
"PODLINGNAMESEARCH",
"POLYGENE",
"PORTALS",
"APA",
"PB",
"PIO",
"PROVISIONR",
"PRC",
"HERMES",
"PULSAR",
"PYLUCENE",
"QPID",
"DISPATCH",
"QPIDJMS",
"PROTON",
"RAMPART",
"RAMPARTC",
"RANGER",
"RATIS",
"RAVE",
"REEF",
"RIVER",
"RYA",
"S2GRAPH",
"SAMOA",
"SAMZA",
"SAND",
"SANDESHA2",
"SANDESHA2C",
"SANTUARIO",
"SAVAN",
"SCOUT",
"SENTRY",
"SERF",
"SM",
"SMX4",
"SMXCOMP",
"SMX4KNL",
"SMX4NMR",
"SHALE",
"SHINDIG",
"SHIRO",
"CASSANDRASC",
"SINGA",
"SIRONA",
"SLIDER",
"SLING",
"SOAP",
"SOLR",
"SPARK",
"SIS",
"SPOT",
"SQOOP",
"STANBOL",
"STEVE",
"STOMP",
"STONEHENGE",
"STRATOS",
"STREAMPIPES",
"STREAMS",
"STR",
"WW",
"SB",
"SITE",
"SVN",
"SUPERSET",
"SYNAPSE",
"SYNCOPE",
"SYSTEMDS",
"TAJO",
"TAMAYA",
"TAPESTRY",
"TAP5",
"TASHI",
"TST",
"TEXEN",
"MMETRIC",
"THRIFT",
"TIKA",
"TILES",
"AUTOTAG",
"TEVAL",
"TREQ",
"TILESSB",
"TILESSHARED",
"TILESSHOW",
"TINKERPOP",
"TOMEE",
"TATPI",
"TOREE",
"TORQUE",
"TORQUEOLD",
"TC",
"TS",
"DIRTSEC",
"TRIPLES",
"TSIK",
"TRB",
"TUSCANY",
"TUWENI",
"UIMA",
"USERGRID",
"VCL",
"VELOCITY",
"VELOCITYSB",
"VELTOOLS",
"VXQUERY",
"VYSPER",
"WADI",
"WAVE",
"WEEX",
"WHIMSY",
"WICKET",
"WINK",
"WODEN",
"WOOKIE",
"WSCOMMONS",
"APOLLO",
"WSRP4J",
"WSS",
"ASFSITE",
"XALANC",
"XALANJ",
"XAP",
"XBEAN",
"XERCESC",
"XERCESP",
"XERCESJ",
"XMLCOMMONS",
"XMLRPC",
"XMLBEANS",
"XGC",
"XMLSCHEMA",
"XW",
"YETUS",
"YOKO",
"ZEPPELIN",
"ZETACOMP",
"ZOOKEEPER"
])