blob: dfb5960183c522b4798dfe7fedd1f2b9c121c247 [file] [log] [blame]
<?php
/*******************************************************************************
* Copyright (c) 2009 Eclipse Foundation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Wayne Beaton (Eclipse Foundation)- initial API and implementation
*******************************************************************************/
/*
* This file assumes that the $App variable has been defined.
*/
require_once(dirname(__FILE__) . "/common.php");
require_once(dirname(__FILE__) . "/debug.php");
trace_file_info(__FILE__);
class CQ {
var $id;
var $project;
var $description;
var $name;
var $state;
var $status;
var $license;
var $resolution;
var $keywords;
var $attachments;
var $bundles = array();
var $parent;
var $piggyback_cqs = array();
function CQ($id, $description, $keywords=array(), $attachments=0) {
$this->id = $id;
$this->description = $description;
$this->keywords = $keywords;
$this->attachments = $attachments;
/*
* Compute a reasonable name for the CQ. We attempt to extract this
* from the description. In the process, we eliminate as much "chaff"
* as possible. The word "Apache" at the front of "Apache Ant" is not
* interesting. Nor is anything in brackets or parentheses considered
* interesting. You get the idea...
*/
$name = $description;
$name = preg_replace('/Apache/i', '', $name);
$name = preg_replace('/Google/i', '', $name);
$name = preg_replace('/\[[^\]]*\]/', '', $name);
$name = preg_replace('/\([^\)]*\)/', '', $name);
$name = preg_replace('/\([^\)]*\)/', '', $name);
$name = preg_replace('/Version\:?.*$/i', '', $name);
$name = preg_replace('/\-?(\d+(\.\d+)*)/i', '', $name);
$name = preg_replace('/\.jar/i', '', $name);
$name = preg_replace('/\sjar/i', '', $name);
$name = preg_replace('/\sv(\s|$)/i', '', $name); // Remove a solitary 'v' (i.e. "version")
$name = preg_replace('/\W+/', ' ', $name); // Replace non-word characters with a single space.
$name = ucwords($name);
$this->name = trim($name);
/*
* Return the version if possible. This value is intended for purposes of sorting
* and other more superficial purposes and should generally not be considered
* 'official' for purposes of comparing CQ equivalency (this may change).
*
* We look for the numbers following the word "Version" in the description. If
* this fails, we look for anything that looks like a version number. Version numbers
* are assumed to be mutli-segment numbers (e.g. '1.32.3').
*/
if (preg_match('/Version\:?\s+(\d+(\.\d+)*)/i', $description, $matches)) {
$this->version = $matches[1];
} else if (preg_match('/(\d+(\.\d+)*)/', $description, $matches)) {
$this->version = $matches[1];
}
}
public function is_root() {
return !$this->parent;
}
public function getId() {
return $this->id;
}
/**
*
* The computed value is cached the first time this method is called.
*/
public function getName() {
return $this->name;
}
public function getVersion() {
return $this->version;
}
public function getDescription() {
return $this->description;
}
public function getLicense() {
return $this->license;
}
public function &get_root($recursion_list = array()) {
if (in_array($this, $recursion_list)) throw new RecursiveAncestryException();
if ($this->parent == $this) return $this;
if (!$this->parent) return $this;
$recursion_list[] = $this;
$parent = $this->parent;
$root = &$parent->get_root($recursion_list);
return $root;
}
/**
* PROVISIONAL
* Enter description here ...
*/
public function get_related() {
$related = array();
$this->get_root()->gather_related($related);
return $related;
}
/**
* This method finds a related CQ for the project with the provided id.
* That is, the receiver and all of it's descendent piggybacks are
* searched to find one that is owned by the given project id.
*
* @param $projectid The id of the project to search for.
* @return An instance of CQ or <code>null</code>.
*/
public function find_cq_for_project($projectid, $check_parents=false, $check_children=false) {
if ($this->project == $projectid) return $this;
foreach ($this->piggyback_cqs as $piggyback) {
$match = $piggyback->find_cq_for_project($projectid);
if ($match) return $match;
}
if ($check_parents) {
$parent = get_project_parent_id($projectid);
if ($parent) {
$match = $this->find_cq_for_project(get_project_parent_id($projectid), true, false);
if ($match) return $match;
}
}
if ($check_children) {
$match = $this->find_cq_for_subproject($projectid);
if ($match) return $match;
}
return null;
}
/* private */ function find_cq_for_subproject($projectid) {
if (is_valid_subproject_id($projectid, $this->project)) {
return $this;
}
foreach ($this->piggyback_cqs as $piggyback) {
$match = $piggyback->find_cq_for_subproject($projectid);
if ($match) return $match;
}
return null;
}
/* private */ function gather_related(&$related) {
$related[$this->id] = $this;
foreach ($this->piggyback_cqs as $piggyback) {
$piggyback->gather_related($related);
}
}
public function as_html() {
return "<a href=\"https://dev.eclipse.org/ipzilla/show_bug.cgi?id=$this->id\">CQ $this->id</a>";
}
/**
* @deprecated
* @see #isContribution()
* return bool
*/
public function is_contribution() {
return $this->isContribution();
}
/**
* Answers true if the receiver represents a contribution. That is, does it
* represent something that has found (or will find) its way into an
* eclipse.org VCS repository?
*
* @return bool
*/
public function isContribution() {
if ($this->hasKeyword('epl')) return true;
if ($this->hasKeyword('projectcode')) return true;
return false;
}
/**
* @deprecated
* @see #isThirdParty()
* @return bool
*/
public function is_third_party() {
return $this->isThirdParty();
}
/**
* Return true if the receiver represents a third-party library, false
* otherwise.
*
* @return bool
*/
public function isThirdParty() {
if ($this->hasKeyword("nonepl")) return true;
if ($this->hasKeyword("thirdparty")) return true;
return false;
}
public function isActive() {
switch ($this->state) {
case 'approved_all_projects':
case 'approved_one_project':
case 'approved':
case 'reuse':
return true;
case 'awaiting_analysis':
case 'awaiting_committer':
case 'awaiting_emo':
case 'awaiting_pmc':
case 'awaiting_project':
case 'awaiting_triage':
case 'new':
case 'under_review':
if ($this->isFixed()) return true;
}
return false;
}
public function isPrereq() {
switch ($this->state) {
case 'exempt_prereq':
case 'workswith':
case 'prereq':
return true;
}
return false;
}
public function isExemptPrereq() {
return $this->state == 'exempt_prereq';
}
public function isWorksWith() {
return $this->state == 'workswith';
}
public function isPending() {
switch ($this->state) {
case 'awaiting_analysis':
case 'awaiting_committer':
case 'awaiting_emo':
case 'awaiting_pmc':
case 'awaiting_project':
case 'awaiting_triage':
case 'new':
case 'under_review':
if (!$this->isFixed()) return true;
}
return false;
}
/**
* A CQ is considered unused if it has any of the following keywords:
* 'unused', 'obsolete', or 'withdrawn'.
*
* @return bool
*/
public function isUnused() {
if ($this->hasKeyword('unused')) return true;
if ($this->hasKeyword('obsolete')) return true;
if ($this->hasKeyword('withdrawn')) return true;
return false;
}
public function isModified() {
return $this->hasKeyword('modified');
}
public function isUnmodified() {
return $this->hasKeyword('modified');
}
public function isSource() {
return $this->hasKeyword('source');
}
public function isBinary() {
return $this->hasKeyword('binary');
}
public function isSourceAndBinary() {
return $this->hasKeyword('sourceandbinary');
}
/**
* @deprecated
* @see #hasKeyword()
* @param string $needle
*/
public function has_keyword($needle) {
return $this->hasKeyword($needle);
}
/**
* Answers true if the receiver has the provided keyword applied to it, or
* false otherwise.
*
* @param string $needle
*/
public function hasKeyword($needle) {
foreach ($this->keywords as $keyword) {
if ($keyword == $needle) return true;
}
return false;
}
public function is_resolved() {
if (!$this->status) return false;
return in_array($this->status, array('RESOLVED', 'VERIFIED', 'CLOSED'));
}
public function isFixed() {
if (!$this->is_resolved()) return false;
return $this->resolution == 'FIXED';
}
public function is_approved() {
if (!$this->is_resolved()) return false;
return $this->state == 'approved';
}
public function is_invalid() {
if (!$this->is_resolved()) return false;
if (!$this->resolution) return false;
return in_array($this->resolution, array('INVALID', 'WONTFIX', 'DUPLICATE', 'WORKSFORME', 'MOVED'));
}
public function add_bundle($bundle, $regex) {
$this->bundles[$bundle] = $regex;
}
public function get_parent_id() {
if (preg_match('/ATO\s+Orbit\s+(\d+)/', $this->description, $matches))
return $matches[1];
if (preg_match('/ATO\s*CQ\s*([0-9]+)/', $this->description, $matches))
return $matches[1];
if (preg_match('/PB\s*CQ\s*([0-9]+)/', $this->description, $matches))
return $matches[1];
if (preg_match('/PB\s*([0-9]+)/', $this->description, $matches))
return $matches[1];
if (preg_match('/Orbit\s*(?:CQ)?\s*([0-9]+)/', $this->description, $matches))
return $matches[1];
// $matches = null;
// preg_match('/CQ[ ]*#?([0-9]*)/', $this->description, $matches);
// if (count($matches) > 1) return $matches[1];
return null;
}
}
define("CQ_SHOW_NONE", 0);
define("CQ_SHOW_ANCESTORS", 1);
define("CQ_SHOW_PIGGYBACKS", 2);
define("CQ_SHOW_BUNDLES", 4);
define("CQ_SHOW_KEYWORDS", 8);
define("CQ_SHOW_ALL", CQ_SHOW_ANCESTORS + CQ_SHOW_PIGGYBACKS + CQ_SHOW_BUNDLES + CQ_SHOW_KEYWORDS);
function to_node($doc, $root, $cq, $options = CQ_SHOW_ALL, $tag = 'cq', $recursion_list=array()) {
$cq_element = $doc->createElement($tag);
$cq_element->setAttribute('id', $cq->id);
$cq_element->setAttribute('name', $cq->getName());
$cq_element->setAttribute('version', $cq->getVersion());
$cq_element->setAttribute('project', $cq->project);
$cq_element->setAttribute('status', $cq->status);
$cq_element->setAttribute('state', $cq->state);
$cq_element->setAttribute('resolution', $cq->resolution);
$cq_element->setAttribute('description', xmlentities($cq->description));
$cq_element->setAttribute('attachments', $cq->attachments);
$cq_element->setAttribute('third-party', $cq->is_third_party() ? "true" : "false");
$cq_node = $root->appendChild($cq_element);
if ($options & CQ_SHOW_KEYWORDS) {
foreach ($cq->keywords as $keyword) {
$keyword_element = $doc->createElement('keyword');
$keyword_element->setAttribute('name', $keyword);
$cq_node->appendChild($keyword_element);
}
}
if ($options & CQ_SHOW_BUNDLES) {
foreach ($cq->bundles as $bundle => $regex) {
$bundle_element = $doc->createElement('bundle');
$bundle_element->setAttribute('id', $bundle);
$bundle_element->setAttribute('regex', $regex);
$cq_node->appendChild($bundle_element);
}
}
if ($options & CQ_SHOW_PIGGYBACKS) {
foreach($cq->piggyback_cqs as $piggyback) {
to_node($doc, $cq_node, $piggyback, CQ_SHOW_NONE, 'piggyback');
}
}
if ($options & CQ_SHOW_ANCESTORS) {
// If this is the second time that we've encountered the receiver
// in the recursion, bail out to avoid an infinite loop.
if (in_array($cq, $recursion_list)) {
$parent_element = $doc->createElement('recursive-parent');
$cq_node->appendChild($parent_element);
return;
}
// If there's a parent, then we're going to recurse. If not, just bail.
if (!$cq->parent) return;
$recursion_list[] = $cq;
to_node($doc, $cq_node, $cq->parent, CQ_SHOW_ANCESTORS, 'parent-cq', $recursion_list);
}
}
class RecursiveAncestryException extends Exception {}
function find_root_cqs() {
$cqs = array();
foreach(find_cqs() as $cq) {
if ($cq->is_root()) $cqs[] = $cq;
}
return $cqs;
}
/**
* This function finds a single CQ. Note that multiple calls to this
* function will return the same object.
*
* @param $id int id of the the CQ to find.
*/
function find_cq($id) {
$cqs = find_cqs();
return $cqs[$id];
}
/**
* This function, curiously enough, finds the known CQs. The computation
* is done exactly once. Multiple calls to this function will return the
* same array.
*
* If this function is called on the server, it finds the CQs from the database.
* If it is called in "Development Mode", it makes a RESTful webservice call
* to the server to get the CQ data.
*
*/
function find_cqs() {
// TODO Consider implementing a completely offline mode.
global $App;
global $_cqs;
if (!isset($_cqs)) $_cqs = $App->devmode ? load_cqs_from_server() : primitive_find_cqs();
return $_cqs;
}
/**
* This function returns the CQ assigned to the project with the provided id
* that contributes the bundle with the provided name.
*/
function &find_cq_for_bundle($bundle, $projectid, $check_parents=false, $check_children=false) {
foreach (find_root_cqs() as $cq) {
foreach($cq->bundles as $pattern => $regex) {
if (@preg_match($regex, $bundle)) {
$match = $cq->find_cq_for_project($projectid, $check_parents, $check_children);
if ($match) return $match;
}
}
}
$match = null;
return $match;
}
function find_cqs_for_bundle($bundle) {
$cqs = array();
foreach (find_root_cqs() as $cq) {
foreach($cq->bundles as $pattern => $regex) {
if (@preg_match($regex, $bundle)) {
$cq->gather_related($cqs);
}
}
}
return $cqs;
}
/**
* This function does the dirty work of actually finding the CQs from the IPZilla Database.
*
* THIS IS NOT PUBLIC API.
*/
function primitive_find_cqs() {
global $App;
$sql = "
SELECT
bugs.bug_id as id,
components.name as project,
bugs.short_desc as description,
bugs.bug_status as status,
bugs.resolution as resolution,
bugs.keywords as keywords,
bugs.bug_severity as state,
bugs.cf_license as license,
count(attachments.attach_id) as attachments
FROM
bugs
join components on (bugs.component_id = components.id)
left join attachments on (bugs.bug_id = attachments.bug_id and attachments.isobsolete = 0)
group by bugs.bug_id";
$result = $App->ipzilla_sql( $sql );
$cqs = array();
while( $row = mysql_fetch_assoc($result) ) {
$id = $row['id'];
$keywords = preg_split('/, */', $row['keywords']);
$cq = new CQ($id,$row['description'],$keywords,$row['attachments']);
$cq->project = $row['project'];
$cq->status = $row['status'];
$cq->resolution = $row['resolution'];
$cq->state = $row['state'];
$cq->license = $row['license'];
$cqs[$id] = $cq;
}
matchParentCQs($cqs);
foreach ($cqs as $cq) {
try {
$root = $cq->get_root();
} catch (RecursiveAncestryException $e) {
continue;
}
if (!$root) continue;
if ($root == $cq) continue;
$root->piggyback_cqs[] = $cq;
}
read_bundle_mappings_file($cqs);
read_orbit_bundle_mappings_file($cqs);
return $cqs;
}
function matchParentCQs(&$cqs) {
$trace = trace("Matching CQs with parents");
$previous = array();
foreach($cqs as $cq) {
$nested = $trace->trace("Finding parent for CQ" . $cq->id . ", " . $cq->description);
$parent_id = $cq->get_parent_id();
if ($parent_id == $cq->id) continue;
if ($parent_id) {
$nested->trace("Parent is indicated: " . $parent_id);
$cq->parent = $cqs[$parent_id];
continue;
}
$description = $cq->getDescription();
foreach($previous as $candidate) {
if ($candidate->getDescription() == $description) {
$nested->trace("Found similar: " . $candidate->id);
$cq->parent = $candidate;
}
}
$previous[] = $cq;
}
}
/**
* This function reads the bundle mappings from the cq-map.txt file
* and--when possible--connects the bundle name with a cq. Each line in the
* text that matches the pattern '[cq#], [bundle name]' is assumed to
* be a valid mapping.
*
* This function returns nothing; it has the side effect of (potentially)
* modifying the CQ instances in the first parameter.
*
* THIS IS NOT PUBLIC API.
*
* @param unknown_type $cqs
*/
function read_bundle_mappings_file(&$cqs) {
$top = trace("Reading bundle mappings.");
$filepath = $_SERVER['DOCUMENT_ROOT'] . '/projects/ip-check/cq-map.txt';
$mappings = file($filepath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach($mappings as $mapping) {
$tracer = $top->trace("Processing bundle mapping '$mapping'.");
$matches = null;
preg_match('/\s*([0-9]+),\s*(.+\.jar)/', $mapping, $matches);
if (count($matches) > 2) {
$id = $matches[1];
$bundle = $matches[2];
$tracer->trace("Found CQ/bundle mapping: CQ $id maps to $bundle");
if (isset($cqs[$id])) {
// First, we find the CQ that corresponds to the mapping
$cq = $cqs[$id];
try {
// Then, we find the root CQ (sometimes the mapping is
// between a piggyback CQ and bundle).
$cq = $cq->get_root();
if ($cq->id != $id) {
$tracer->trace("CQ $id has root $cq->id");
}
} catch (RecursiveAncestryException $e) {
$tracer->trace("Cannot find root for CQ $cq.");
continue;
}
$root = $cq->get_root();
/*
* The regex patterns formed below have a start delimiter (^)
* but no end delimiter ($) to allow for matches of files with
* extra junk (e.g. .jar.pack.gz) at the end. This may end up
* being more flexible than we need.
*/
$matches = null;
preg_match('/^([a-zA-Z0-9\.\-\_]+)[_-]((?:\d+\.){3})(.*\.)?jar/', $bundle, $matches);
if (count($matches) > 0) {
$pattern = $matches[1] . '_' . $matches[2];
$regex = $matches[1] . '[_-]' . $matches[2];
$regex = str_replace('.', '\.', $regex);
$regex = '/^' . $regex . '(.*\.)?jar/';
$pattern = "$pattern*.jar";
$root->add_bundle($pattern, $regex);
$tracer->trace("Bundle '$bundle' becomes pattern '$pattern' ($regex).");
// Only generate a source bundle mapping if this isn't a source bundle.
if (!preg_match('/\.source$/', $matches[1])) {
$sourcePattern = $matches[1] . '.source_' . $matches[2];
$sourceRegex = $matches[1] . '.source[_-]' . $matches[2];
$sourceRegex = str_replace('.', '\.', $sourceRegex);
$sourceRegex = '/^' . $sourceRegex . '(.*\.)?jar/';
$sourcePattern = "$sourcePattern*.jar";
$root->add_bundle($sourcePattern, $sourceRegex);
$tracer->trace("Source for bundle '$bundle' becomes pattern '$sourcePattern' ($sourceRegex).");
}
} else {
$pattern = $bundle;
$regex = str_replace('.', '\.', $pattern);
$regex = "/^$regex/";
$tracer->trace("Bundle '$bundle' becomes pattern '$pattern' ($regex).");
$root->add_bundle($pattern, $regex);
}
} else {
$tracer->trace("CQ $id not found!");
}
} else {
$tracer->trace("Skipping CQ-bundle-mapping: $mapping (incomplete information)");
}
}
}
/**
* This function reads the bundle mappings from Orbit. Note that this
* does not read directly from Orbit, but rather from a dump of information
* extracted from Orbit in JSON format.
*
* This function returns nothing; it has the side effect of (potentially)
* modifying the CQ instances in the first parameter.
*
* THIS IS NOT PUBLIC API.
*
* @param unknown_type $cqs
*/
function read_orbit_bundle_mappings_file(&$cqs) {
// TODO Refactoring opportunities abound.
$top = trace("Reading Orbit bundle mappings.");
$filepath = dirname(__FILE__) . '/../ip-check/orbit-cq-map.json';
$orbit = json_decode(file_get_contents($filepath), TRUE);
foreach($orbit as $bundle => $data) {
$tracer = $top->trace("Processing bundle mapping '$bundle'.");
if ($id = $data['cq']) {
$tracer->trace("Found CQ/bundle mapping: CQ $id maps to $bundle");
if (isset($cqs[$id])) {
// First, we find the CQ that corresponds to the mapping
$cq = $cqs[$id];
try {
// Then, we find the root CQ (sometimes the mapping is
// between a piggyback CQ and bundle).
$cq = $cq->get_root();
if ($cq->id != $id) {
$tracer->trace("CQ $id has root $cq->id");
}
} catch (RecursiveAncestryException $e) {
$tracer->trace("Cannot find root for CQ $cq.");
continue;
}
$root = $cq->get_root();
/*
* The regex patterns formed below have a start delimiter (^)
* but no end delimiter ($) to allow for matches of files with
* extra junk (e.g. .jar.pack.gz) at the end. This may end up
* being more flexible than we need.
*/
$matches = null;
preg_match('/^([a-zA-Z0-9\.\-\_]+)[_-]((?:\d+\.){3})(.*\.)?jar/', $bundle, $matches);
if (count($matches) > 0) {
$pattern = $matches[1] . '_' . $matches[2];
$regex = $matches[1] . '[_-]' . $matches[2];
$regex = str_replace('.', '\.', $regex);
$regex = '/^' . $regex . '(.*\.)?jar/';
$pattern = "$pattern*.jar";
$root->add_bundle($pattern, $regex);
$tracer->trace("Bundle '$bundle' becomes pattern '$pattern' ($regex).");
// Only generate a source bundle mapping if this isn't a source bundle.
if (!preg_match('/\.source$/', $matches[1])) {
$sourcePattern = $matches[1] . '.source_' . $matches[2];
$sourceRegex = $matches[1] . '.source[_-]' . $matches[2];
$sourceRegex = str_replace('.', '\.', $sourceRegex);
$sourceRegex = '/^' . $sourceRegex . '(.*\.)?jar/';
$sourcePattern = "$sourcePattern*.jar";
$root->add_bundle($sourcePattern, $sourceRegex);
$tracer->trace("Source for bundle '$bundle' becomes pattern '$sourcePattern' ($sourceRegex).");
}
} else {
$pattern = $bundle;
$regex = str_replace('.', '\.', $pattern);
$regex = "/^$regex/";
$tracer->trace("Bundle '$bundle' becomes pattern '$pattern' ($regex).");
$root->add_bundle($pattern, $regex);
}
} else {
$tracer->trace("CQ $id not found!");
}
} else {
$tracer->trace("Skipping CQ-bundle-mapping: $bundle (incomplete information)");
}
}
}
/**
* PROVISIONAL
* @param $id
*/
function find_root_cq($id) {
$cqs = find_cqs();
if (!isset($cqs[$id])) return null;
$cq = $cqs[$id];
if (!$cq) return null;
return $cq->get_root();
}
/**
* PROVISIONAL
* @param $ids
* @param $projectid
*/
function find_related_cq($ids, $projectid) {
foreach ($ids as $id) {
$cq = find_root_cq($id);
if (!$cq) continue;
$related = $cq->find_cq_for_project($projectid);
if ($related) return $related;
}
return null;
}
/**
* Force the CQs to be loaded from the server via HTTP. Otherwise, CQs will be lazily
* loaded from the database when they are required.
*/
function load_cqs_from_server($file = "http://www.eclipse.org/projects/xml/cqs.php") {
global $_cqs;
$_cqs = array();
$raw = simplexml_load_file($file);
foreach($raw->cq as $item) {
$id = (int)$item['id'];
$description = $item['description'];
$description = "$description";
$cq = new CQ($id, $description);
$cq->project = $item['project'];
$cq->status = $item['status'];
$cq->state = $item['state'];
foreach ($item->keyword as $keyword) {
$cq->keywords[] = $keyword['name'];
}
foreach ($item->bundle as $bundle) {
$cq->add_bundle((String)$bundle['id'], (String)$bundle['regex']);
}
$_cqs[$id] = $cq;
}
foreach($raw->cq as $item) {
$id = (int)$item['id'];
$cq = $_cqs[$id];
foreach ($item->piggyback as $piggyback) {
$cq->piggyback_cqs[] = $_cqs[(int)$piggyback['id']];
}
}
//read_bundle_mappings_file($cqs);
return $_cqs;
}
?>