blob: f3f39c032df66d1adb32bd5aee9b8b192b7e7c1a [file] [log] [blame]
<?php
/*******************************************************************************
* Copyright (c) 2010, 2017 Eclipse Foundation and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
require_once (dirname(__FILE__) . "/Forge.class.inc");
require_once (dirname(__FILE__) . "/License.class.inc");
require_once (dirname(__FILE__) . "/database.inc");
require_once (dirname(__FILE__) . "/debug.php");
require_once (dirname(__FILE__) . "/common.php");
define('PROJECT_LIVELINESS_NEVER_ACTIVE', 0);
define('PROJECT_LIVELINESS_ACTIVE', 1);
define('PROJECT_LIVELINESS_STALE', 2);
define('PROJECT_LIVELINESS_INACTIVE', 2);
define('PROJECT_LIVELINESS_DEAD', 4);
define('PROJECT_REPO_ALL_GIT', 1);
define('PROJECT_REPO_SOME_GIT', 2);
define('PROJECT_REPO_NO_GIT', 3);
define('PROJECT_REPO_NONE', 4);
define('TRADEMARK_REGISTERED', 1);
define('TRADEMARK_UNREGISTERED', 2);
define('TRADEMARK_FILED', 3);
define('TRADEMARK_ALL', TRADEMARK_REGISTERED | TRADEMARK_UNREGISTERED);
/**
* Get a project from the PMI instead of the foundation database.
* Going directly to the PMI is no longer necessary now that the
* implementation has been cleaned up. Now, we can just load the
* project; if information is required from the PMI, it will be
* loaded from there.
*
* @deprecated use Project::getProject()
* @param string $id
* a project id of the form 'technology.egit'
* @return PMIProject
*/
function get_project_from_pmi($id) {
return Project::getProject($id);
}
/**
* Answers an array containing all top-level projects.
* Only
* active projects are included.
*
* @deprecated Use Project::getTopLevelProjects()
* @return Project[]
*/
function getTopLevelProjects() {
PMIProject::preload();
return Project::getTopLevelProjects();
}
/**
* This function returns an instance of Project representing the project
* with the provided $id.
* Note that this function preserves object
* identity: if called twice with the same value, it will return the
* exact same object.
*
* @deprecated Use Project::getProject($id)
* @return Project
*/
function getProject($id) {
return Project::getProject($id);
}
/**
*
* @deprecated Use Project::getProject($id)
* @return Project
*/
function get_project($id) {
return Project::getProject($id);
}
/**
* This function returns all the active projects.
*
* @deprecated Use Project::getActiveProjects()
* @return Project[]
*/
function getActiveProjects() {
return Project::getActiveProjects();
}
/**
* Instances of the subclasses of Project represent a single Eclipse Foundation
* Project.
* Projects may be in any of the forges supported by the Eclipse
* Foundation.
*
* This class has two subclasses. The ProductProxy class is basically the
* entry-point; when you ask for a project, you first get one of these. These
* instances are pretty cheap to make and can answer a lot of questions. When
* requests are made that require more information, that information is
* gathered from the PMI and results in the creation of a PMIProduct instance
* (loading from the PMI is pretty expensive, so we try to avoid it when we
* can).
*
* The entry points are all static methods on this class. Instances should
* never be created using the constructors.
*
* Note that instances are cached as they are loaded; subsequent queries
* will always return the cached instance.
*/
abstract class Project {
static $config;
static $projects = array();
/**
* Initialize the class by loading the configuration from
* projects.ini. The projects.ini file includes information about
* how projects have moved (e.g. 'technology.ecf' became 'rt.ecf'),
* information about Top Level Project specific brands, etc.
*/
static function init() {
self::$config = parse_ini_file(dirname(__FILE__) . '/projects.ini');
}
/**
* Get all projects regardless of whether or not they are active
* or archived. The Foundation database is used to get this information.
* This implementation takes project moves that occurred in the day
* when we duplicated information (e.g. there are records in the
* database for 'technology.ecf' and 'rt.ecf') into consideration.
* A project that has been moved will only be represented once in
* this query.
*
* This move information is represented in the projects.ini file.
*
* Other pseudo projects are also skipped by this query.
*
* @return ProjectProxy[]|mixed[]
*/
public static function getAll() {
// Skip all pseudo projects.
$skip = array("'root'", "'foundation-internal'", "'galileo'");
// Skip all projects that were duplicated in a move operation.
// TODO Consider encapsulating this access of the static field.
foreach(self::$config['move'] as $from => $to) {
$skip[] = "'{$from}'";
}
$skip = implode(',', $skip);
return self::getProjects(array(
"IsComponent = 0",
"(p.ProjectID not in ($skip))",
"(p.ParentProjectID not in ('foundation-internal'))"
));
}
public static function getProject($id) {
if (!isValidProjectId($id))
return null;
$projects = self::getProjects(array(
"(p.ProjectId = '$id')"
));
if ($projects)
return $projects[$id];
return null;
}
public static function getAllProjects($ids) {
$all = array();
foreach($ids as $id) {
if (isValidProjectId($id)) {
$all[] = "'{$id}'";
}
}
$all = implode(',', $all);
return self::getProjects(array(
"(p.ProjectId in ($all))"
));
}
private static function getAlternateForgeIds() {
$forges = array();
foreach (Forge::getForges() as $forge) {
if (!$forge->isEclipseForge())
$forges[] = "'{$forge->getId()}'";
}
return implode(',', $forges);
}
public static function getTopLevelProjects() {
$forges = self::getAlternateForgeIds();
$where = array(
"p.IsActive",
"(p.ProjectID not in ('root', 'foundation-internal', 'galileo'))",
"(p.ParentProjectID not in ('foundation-internal'))");
if ($forges)
$where[] = "((p.ParentProjectID = 'root' and p.ProjectId not in ($forges)) or p.ParentProjectID in ($forges))";
else
$where[] = "p.ParentProjectID = 'root'";
return self::getProjects($where);
}
public static function getActiveProjects() {
$forges = self::getAlternateForgeIds();
$where = array(
"p.IsActive",
"(p.ProjectID not in ('root', 'foundation-internal', 'galileo'))",
"(p.ParentProjectID not in ('foundation-internal'))");
if ($forges)
$where[] = "(p.ProjectId not in ($forges))";
return self::getProjects($where);
}
public static function getProjectsForCommitter($id) {
$forges = self::getAlternateForgeIds();
$where = array(
"(p.ProjectID in (SELECT ProjectID FROM PeopleProjects where PersonId='$id' and Relation='CM' and InactiveDate is null))",
"p.IsActive"
);
if ($forges)
$where[] = "(p.ProjectId not in ($forges))";
return self::getProjects($where);
}
/**
* This function answers an array containing all those projects that
* use the license with the provided id.
*
* @param string $id the license id as expressed in the Foundation DB (e.g. 'EPL-2.0')
* @return Project[]
*/
public static function getProjectsWithLicense($id) {
return self::getProjects(array(
"p.ProjectId in (select ProjectId from ProjectLicenses where LicenseId='{$id}')",
"p.IsActive"
));
}
public static function getSubprojects($id) {
return self::getProjects(array(
"(p.ParentProjectId = '$id')",
"p.IsActive"
));
}
public static function visit($function, $pre, $post) {
$pre(null);
foreach (self::getTopLevelProjects() as $project) {
$project->visitHierarchy($function, $pre, $post, 0);
}
$post(null);
}
private static function getProjects($conditions) {
$where = join(' and ', $conditions);
$sql = "
SELECT distinct
p.ProjectId as id, p.Name as name, p.ParentProjectID as parent, p.ProjectPhase as phase,
p.UrlDownload,
group_concat(distinct l.LicenseId) as licenses,
min(pp.ActiveDate) as provisioned_date,
group_concat(distinct cm.PersonId) as committer_ids,
group_concat(distinct pl.PersonId) as lead_ids
FROM Projects as p
left join ProjectLicenses as l on p.ProjectId=l.ProjectId
left join PeopleProjects as pp on p.ProjectId=pp.ProjectId
left join PeopleProjects as cm on p.ProjectId=cm.ProjectId and cm.Relation='CM' and cm.InactiveDate is null
left join PeopleProjects as pl on p.ProjectId=pl.ProjectId and pl.Relation='PL' and pl.InactiveDate is null
where
$where
group by p.ProjectId";
$projects = array();
query('foundation', $sql, null, function ($row) use (&$projects) {
$id = $row['id'];
if (isset(Project::$projects[$id])) {
$projects[$id] = Project::$projects[$id];
}
else {
$project = new ProjectProxy($id, $row);
Project::$projects[$id] = $project;
$projects[$id] = $project;
}
});
// The configuration defines projects that should be removed
// from query results. Remove those projects.
// TODO Consider encapsulating this access of the static field.
foreach(self::$config['remove'] as $remove) {
unset($projects[$remove]);
}
return $projects;
}
private function __construct() {
}
/**
* Answer the raw name of the receiver. This is
* the name as it appears in the data. We almost
* never want to use this directly. Instead, use
* the nick name or formal name.
*
* @see Project::getNickName()
* @see Project::getFormalName()
*/
public abstract function getName();
public abstract function getId();
public abstract function getParentId();
public abstract function getProjectPhase();
public abstract function getProxy();
/**
* The short id is the last segment in the fully-qualified
* id. e.g. the short id of 'technology.egit' is 'egit'.
*
* @return string
*/
public function getShortId() {
preg_match("/([^\.]+)$/", $this->getId(), $matches);
return $matches[1];
}
public function getLocalId() {
return $this->getForge()->getLocalProjectId($this->getId());
}
public function getShortName() {
return $this->getName();
}
public function getChildren() {
return self::getSubprojects($this->getId());
}
public function getParent() {
return self::getProject($this->getParentId());
}
public function isInIncubationPhase() {
return in_array($this->getProjectPhase(), array(
'Incubation',
'Incubation.nc'
));
}
/**
* Components are an old concept.
* Nothing is a component anymore.
*
* @return boolean Always false
*/
public function isComponent() {
return false;
}
public function isArchived() {
return $this->getProjectPhase() == 'Archived';
}
public function isInIncubationConformingPhase() {
return $this->getProjectPhase() == 'Incubation';
}
public function isInIncubationNonConformingPhase() {
return false;
}
public function getUrl() {
$id = $this->getLocalId();
$base = $this->getForge()->getUrl();
return "{$base}/projects/{$id}";
}
/**
* This function answers the URL to use to get more data
* about the project in JSON format, including releases, etc.
*/
public function getDataUrl() {
if (!$forge = Forge::getForgeForProjectId($this->getId())) {
return null;
}
$base = $forge->getUrl();
$local = $forge->getLocalProjectId($this->getId());
return "{$base}/json/project/$local";
}
public function getTopLevelProject() {
if ($this->isTopLevel())
return $this;
if (!$parent = $this->getParent())
return $this;
return $parent->getTopLevelProject();
}
public function isTopLevel() {
if (!$forge = $this->getForge())
return true;
if ($forge->isEclipseForge())
return $this->getParentId() == 'root';
return $this->getParentId() == $forge->getId();
}
public function getForge() {
return Forge::getForgeForProjectId($this->getId());
}
public function visitHierarchy($function, $pre, $post, $level) {
$function($this, $level);
$pre($this);
foreach ($this->getChildren() as $child) {
$child->visitHierarchy($function, $pre, $post, $level + 1);
}
$post($this);
}
/**
* This method answers the formal name of the project. If the
* project's name does not include either 'Eclipse', or the
* brand name associated with its Top Level Project, then the
* brand name is prepended to the project name.
*
* Remove anything that's between parentheses. We actually
* look for a space followed by an open parenthesis, because
* some projects (e.g. "Eclipse e(fx)clipse") include them
* in their proper name (and "Eclipse eclipse" just looks weird).
*
* e.g.
* <li>"EGit" becomes "Eclipse EGit"</li>
* <li>"e(fx)clipse" becomes "Eclipse e(fx)clipse"</li>
* <li>"Eclipse Communication Framework (ECF)" becomes "Eclipse Communication Framework"</li>
* <li>"PolarSys Capella" becomes "PolarSys Capella"</li>
* <li>"Capella" becomes "PolarSys Capella"</li>
*
* @see Project::getNickName()
*
* @return string
*/
public function getFormalName() {
$brand = $this->getBrandName();
$name = trim(preg_replace('/ \([^\)]*\)/', ' ', $this->getName()));
if (!preg_match("/(?:$brand|Eclipse)/", $name)) {
return "$brand $name";
}
return $name;
}
/**
* Sort out the project's brand. If the project name starts with one of our brands,
* as defined in the projects.ini file, then that is the project's brand.
* The project's brand might also be determined by the top-level project
* (a top-level project may have a brand associated with it; in this case,
* all projects that fall under that top-level project use that brand.
*
* @return string
*/
public function getBrandName() {
foreach(@self::$config['brands'] as $brand) {
if (preg_match("/^{$brand}/", $this->getName())) return $brand;
}
if ($brand = @self::$config['tlp'][$this->getTopLevelProject()->getId()])
return $brand;
return 'Eclipse';
}
/**
* This method answers the nickname for the project.
* The name of the project as listed in the foundation database is the best
* source of this information. This is difficult to guess as sometimes the
* forge name is part of the nick name and sometimes it's not. Values are
* stored in the foundation database as the nickname with optional acronym in
* parentheses. This method strips anything in parentheses from the returned
* value.
*
* We actually look for a space followed by an open parenthesis, because
* some projects (e.g. "Eclipse e(fx)clipse") include them
* in their proper name (and "Eclipse eclipse" just looks weird).
*
* @see Project::getFormalName()
*
* @return string
*/
public function getNickName() {
// We get the proxy and ask it for the name.
return trim(preg_replace('/ \([^\)]*\)/', ' ', $this->getProxy()->getName()));
}
/**
*
* @deprecated
*
* @return NULL
*/
public function getNoteworthyUrl() {
return null;
}
/**
*
* @deprecated
*
*/
public function getNewsgroups() {
return array();
}
/**
*
* @deprecated
*
* @return null|Proposal
*/
public function getProposal() {
// FIXME Implement. Or don't.
return null;
}
public function getLiveliness() {
if (!$this->getCommitActivity())
return PROJECT_LIVELINESS_NEVER_ACTIVE;
if ($this->hasRecentCommitActivity(3))
return PROJECT_LIVELINESS_ACTIVE;
if ($this->hasRecentCommitActivity(6))
return PROJECT_LIVELINESS_STALE;
if ($this->hasRecentCommitActivity(12))
return PROJECT_LIVELINESS_INACTIVE;
return PROJECT_LIVELINESS_DEAD;
}
/**
* Answers whether or not the receiver has recent commit activity.
*
* @param int $months
* (Optional) how many months in the past to check.
* @return boolean
*/
public function hasRecentCommitActivity($months = 6) {
$activity = $this->getCommitActivity();
$date = strtotime('now');
while ($months) {
if (@$activity[date('Ym', $date)])
return true;
$date = strtotime("-1 month", $date);
$months--;
}
return false;
}
/**
* Returns the commit activity for the receiver in the form of an
* array mapping period to commit count.
*
* e.g.,
*
* array('201001' => 52, '201002' => 33, ...)
*
* Note that this information is cached. The first time this method
* is called (on any instance), the cache is created.
*
* @returns [] array mapping period to commit count.
*/
public function getCommitActivity() {
if (isset($this->_commitActivity))
return $this->_commitActivity;
$id = $this->getId();
$sql = "select period, count from ProjectCommitActivity where project='$id'";
$this->_commitActivity = array();
query('dashboard', $sql, array(), function ($row) {
$this->_commitActivity[$row['period']] = $row['count'];
});
return $this->_commitActivity;
}
}
/**
* This class implements a lightweight stand-in to represent a
* project.
* The basic idea is that it is far less expensive to
* query the Foundation Database for information about projects
* than it is to query the PMI. However, the PMI has a lot more
* information. Instances of this class provide as much information
* as they can, but then defer to the PMI when more information is
* required.
*
* @see Project
*/
class ProjectProxy extends Project {
private $id;
private $data;
private $project;
public function __construct($id, $data) {
$this->id = $id;
$this->data = $data;
}
public function __toString() {
return "Project ({$this->getId()})";
}
public function getId() {
return $this->id;
}
public function getParentId() {
return $this->data['parent'];
}
public function getProxy() {
return $this;
}
public function getName() {
return preg_replace ( '/ Root$/', '', $this->data ['name'] );
}
public function getProjectPhase() {
return $this->data['phase'];
}
/**
* We don't directly track the date of project provisioning.
* Rather, we leverage a feature of the provisioning process by
* which the provisioning process is triggered when the first
* committer is ready to be assigned to the project (this is
* figured into the query that we pull from the Foundation DB).
*
* This function returns our best guess at the provisioning date
* in UNIX date format, or null if no such date can be determined.
*
* @see Project::getProjects
* @return NULL|number
*/
public function getProvisionedDate() {
if (!isset($this->data['provisioned_date'])) return null;
return strtotime($this->data['provisioned_date']);
}
public function hasProjectLeads() {
return !empty($this->data['lead_ids']);
}
public function hasCommitters() {
return !empty($this->data['committer_ids']);
}
public function getSpecificationWorkingGroupName() {
$apiData = $this->getAPIData();
return @$apiData[0]['spec_project_working_group']['name'];
}
public function isSpecificationProject() {
$apiData = $this->getAPIData();
return !empty($apiData[0]['spec_project_working_group']);
}
/**
* Get the data related to this project from the Eclipse API.
*
* @return array|mixed
*/
private function getAPIData() {
if (@$this->apiData === NULL) {
$url = 'https://projects.eclipse.org/api/projects/' . preg_replace('/\./','_',$this->getId());
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$this->apiData = json_decode(curl_exec($ch), true);
curl_close($ch);
if (!$this->apiData) $this->apiData = array();
}
return $this->apiData;
}
/**
*
* @deprecated
* @return License[]
*/
public function getLicenses() {
return License::getLicensesForProject($this->getId());
}
/**
* Answer a statement (suitable for use in a file header)
* that describes the licenses of the project.
*
* @deprecated Use License::getLicensesStatement
* @return string
*/
public function getLicensesStatement() {
return License::getLicensesStatement($this->getLicenses());
}
/**
* @deprecated Use License::getSPDXLicenseExpression
* @return string
*/
public function getSPDXLicenseExpression() {
return License::getSPDXExpression($this->getLicenses());
}
/**
* @deprecated Use License::getDefaultFileHeader
* @return string
*/
public function getDefaultFileHeader() {
return License::getDefaultFileHeader($this->getLicenses());
}
/**
* @deprecated Use License::getAlternativeFileHeader
* @return string
*/
public function getAlternativeFileHeader() {
return License::getAlternativeFileHeader($this->getLicenses());
}
/**
* Answers an array containing the trademarks that are known
* to be associated with the project.
*
* @return string[]
*/
function getTrademarks($type = TRADEMARK_ALL, $includeForge = false) {
$trademarks = array();
if ($type & TRADEMARK_UNREGISTERED) {
$this->trademarksDo($type, function($value) use (&$trademarks) {
$trademarks[] = $value;
});
}
return $trademarks;
}
/**
* Iterate over the trademarks that are known to be associated
* with the project.
*
* The primary source of information is the name of the project
* as it is represented in the Foundation Database. If the project
* name includes something in parentheses, then this implementation
* assumes that something is a nickname or alternative name. Or, if there
* are two names separated by <space><dash><space> they are treated
* as separate names.
*
* Further, the configuration (projects.ini) defines zero or more "brands"
* (e.g. 'LocationTech' or 'PolarSys') that are connected to a particular
* Top Level Project. When the brand applies to a project and is not
* already represented in the name, the brand is prepended to the name.
* 'Eclipse' is the default brand.
*
* The number of trademarks that may be associated with a project
* is open-ended. The callable may be executed any number (or zero)
* times.
*
* e.g.
* <li>'LocationTech GeoGig' maps to 'LocationTech GeoGig'</li>
* <li>'GeoGig' maps to 'LocationTech GeoGig' and 'GeoGig'</li>
* <li>'Eclipse EMF' maps so 'EMF'</li>
* <li>'EMF' maps to 'Eclipse EMF' and 'EMF'</li>
*
* @see Project::init()
* @see Project::normalizedTrademarksDo()
*
* @param string $type TRADEMARK_REGISTERED | TRADEMARK_UNREGISTERED
* @param callable $function Function that takes a single argument.
*/
function trademarksDo($type, $function) {
if (preg_match('/(?:releng|website)$/', $this->getId())) return;
$name = $this->getName();
// TODO Deal with different trademark types.
if (preg_match('/^([^\\(]+)\s\\(([^\\)]+)\\)/', $name, $matches)) {
$this->normalizedTrademarksDo($function,$matches[1]);
$this->normalizedTrademarksDo($function,$matches[2]);
} elseif (preg_match('/^([^-]*)\s\\-\s([^-]*)$/', $name, $matches)) {
$this->normalizedTrademarksDo($function,$matches[1]);
$this->normalizedTrademarksDo($function,$matches[2]);
} else {
$this->normalizedTrademarksDo($function,$name);
}
if ($extra = @$this::$config['trademarks'][$this->getId()]) {
call_user_func($function, $extra);
}
}
/**
* Execute the function with each of the normalized trademarks extracted
* from a term. The normalized trademarks for a particular
* term are the term itself, along with the term prefixed
* (if it does not already include) with the corresponding brand.
*
* e.g.
* <li>"EclipseLink" normalizes as "EclipseLink"
* <li>"EGit" normalizes as "EGit" and "Eclipse EGit"
* <li>"Capella" normalizes as "Capella" and "PolarSys Capella"
*
* This script uses the branding information provided in
* the project.ini file to map the Top Level project of the
* receiver to a brand (default is "Eclipse").
*
* @see Project::getBrandName()
* @see Project::getFormalName()
* @see ProjectProxy::trademarksDo()
*
* @param callable $function
* @param string $term The project name (e.g. 'EGit')
*/
private function normalizedTrademarksDo($function, $term) {
$term = trim($term);
if (empty($term)) return;
$term = preg_replace('/\s?\\[[^\\]]*\\]/','', $term);
// First check to see if the name uses one of the general brands;
// if so, then that's the project name. When one of the general
// brands does not apply, check for use of the TLP-specific
// brand; when that brand is not present, both the name from the
// database and that name prepended with the TLP-specific brand
// are considered trademarks.
$brands = @self::$config['brands'];
$brands=implode('|', $brands);
if (preg_match("/^(?:{$brands})/", $term)) {
call_user_func($function, "{$brand} {$term}");
} else {
$brand = $this->getBrandName();
if (!preg_match("/(?:{$brand})/", $term)) {
call_user_func($function, "{$brand} {$term}");
}
call_user_func($function, $term);
}
return;
}
public function getDownloadUri() {
if (!$url = $this->data['UrlDownload'])
return null;
// TODO We should probably ask the forge for the downloadUrl
$forge = $this->getForge()->getId();
if (!preg_match("/^https?:\/\/download.{$forge}.org\/(.*)$/", $url, $matches))
return null;
return $matches[1];
}
public function __call($name, $arguments) {
if (!$project = $this->getPMIProject())
return null;
return call_user_func_array(array(
$project,
$name
), $arguments);
}
private function getPMIProject() {
if ($this->project === null) {
$project = PMIProject::getProject($this->getId());
$this->project = $project ? $project : FALSE;
}
return $this->project;
}
}
/**
* Representation of an Eclipse Foundation project with information
* obtained from a PMI instance.
* Instances of this class should rarely
* be made directly; use the static methods on Project to obtain
* instances.
*
* @see Project
*/
class PMIProject extends Project {
private $data;
private $forge;
private static $cache = array();
/**
* Obtain an representation of a project from a PMI instance.
*
* This method should not generally be used directly. Instead,
* use the static methods on Project to obtain a reference to a project
* and that reference will make this call if it is required.
*
* @param string $id
* @return NULL|PMIProject
*/
public static function getProject($id) {
if (isset(self::$cache[$id]))
return self::$cache[$id];
if (!$forge = Forge::getForgeForProjectId($id))
return null;
$base = $forge->getUrl();
$local = $forge->getLocalProjectId($id);
$url = "{$base}/json/project/$local";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$json = curl_exec($ch);
curl_close($ch);
if (!$json)
return null;
if (!$all = json_decode($json, true))
return null;
if (!$data = $all['projects'][$local])
return null;
return new PMIProject($data, $forge);
}
/**
* @deprecated
*/
public static function preload() {
}
private function __construct($data, $forge) {
$this->data = $data;
$this->forge = $forge;
}
/**
* Answer the project's id as it appears in the Foundation Database.
* The PMI data contains the local id. If the project comes from an
* alternate forge, then the forge id is prepended to the local value.
*
* @return string
*/
public function getId() {
return $this->getForge()->getFoundationDBId($this->getLocalId());
}
public function getLocalId() {
return $this->data['id'][0]['value'];
}
public function getForge() {
return $this->forge;
}
public function getProxy() {
// TODO Consider caching this value
return Project::getProject($this->getId());
}
public function getName() {
// TODO Get the name (when it's available)
return $this->data['title'];
}
public function getDescription() {
if (!$field = @$this->data['description'][0])
return null;
if (!empty($field['summary']))
return $field['summary'];
if (!empty($field['safe_value']))
return $field['safe_value'];
$value = $field['value'];
return explode("\n", $value)[0];
}
public function getScope() {
if (!$field = @$this->data['scope'][0])
return null;
if (!empty($field['summary']))
return $field['summary'];
return @$field['safe_value'];
}
/**
* Answer the project phase.
* In the PMI, this is represented in
* the "state" field. Two out of three values match what we do
* in the Foundation Database. For the third, "Incubating", we
* change it "Incubation" for consistency.
*
* @return string
*/
public function getProjectPhase() {
$state = $this->data['state'][0]['value'];
if ($state == 'Incubating')
return 'Incubation';
return $state;
}
public function getDevListUrl() {
return $this->data['dev_list']['url'];
}
public function getPlanUrl() {
return @$this->data['plan_url'][0]['url'];
}
public function getWikiUrl() {
return @$this->data['wiki_url'][0]['url'];
}
public function getDocumentationUrl() {
return @$this->data['documentation_url'][0]['url'];
}
public function getDownloadsUrl() {
return @$this->data['download_url'][0]['url'];
}
public function getGettingStartedUrl() {
return @$this->data['gettingstarted_url'][0]['url'];
}
/**
* Answer an array containing information about the source
* repositories registered for the project.
*
* @return SourceRepository[]
*/
public function getSourceRepositories() {
$repositories = array();
foreach ($this->data['source_repo'] as $repo) {
$repositories[] = new SourceRepository($this, $repo);
}
return $repositories;
}
public function getBugzillaProduct() {
foreach ($this->data['bugzilla'] as $record) {
if (strlen($record['product']) > 0)
return $record['product'];
}
return null;
}
public function getBugzillaComponents() {
$components = array();
foreach ($this->data['bugzilla'] as $record) {
if (strlen($record['component']) > 0)
$components[] = $record['component'];
}
return $components;
}
public function getMailingLists() {
// FIXME Implement this
return array();
}
/**
* Answer the project's parent's id as it appears in the Foundation Database.
* The PMI data contains the local id. If the project comes from an
* alternate forge, then the forge id is prepended to the local value.
*
* @return string
*/
public function getParentId() {
return $this->getForge()->getFoundationDBId(@$this->data['parent_project'][0]['id']);
}
/**
*
* @deprecated
*
* @return string
*/
function getProjectUrl() {
return $this->getUrl();
}
/**
* Answer an array of information regarding project releases.
*
* @return PMIRelease[]
*/
function getReleases() {
$releases = array();
if (isset($this->data['releases'])) {
foreach ($this->data['releases'] as $data) {
$releases[] = new PMIRelease($this, $data);
}
}
return $releases;
}
public function getReviews() {
// FIXME Consider implementing this.
return array();
}
/**
* Answers the release that falls after a particular time.
* If a date is not provided in the parameter, the current
* date and time is assumed.
*
* @param int $time
* UNIX date.
*/
public function getNextRelease($since = null) {
if (!$since)
$since = time();
$releases = $this->getReleases();
usort($releases, function ($a, $b) {
return $a->getDate() == $b->getDate() ? 0 : ($a->getDate() < $b->getDate() ? -1 : 1);
});
foreach ($releases as $release) {
if ($release->getDate() > $since)
return $release;
}
return null;
}
}
class PMIRelease {
private $project;
private $data;
function __construct($project, $data) {
$this->project = $project;
$this->data = $data;
}
public function getTitle() {
return $this->data['title'];
}
public function getName() {
return $this->getTitle();
}
/**
* Answers the UNIX date of the release.
*
* @return int
*/
public function getDate() {
foreach ($this->data['date'] as $data) {
return strtotime($data['value']);
}
return null;
}
/**
* Answers the URL for the release record.
* If the information
* is provided with the PMI data, then return that value. Otherwise,
* make a very good guess at the value using the project URL and
* the standard pattern for naming releases.
*
* Most of the time, the guess will be correct. But since the PMI
* uses pathauto and there some cases where the project team e.g.
* creates two releases with the same name, there is some risk that
* the URL will be invalid. This risk is relatively low, however,
* as we we usually (always?) have a URL from the PMI.
*
* @return string
*/
public function getUrl() {
if ($url = $this->data['url'])
return $url;
$url = $this->project->getUrl();
$title = $this->getTitle();
return "$url/releases/$title";
}
public function getNoteworthyUrl() {
return @$this->data['noteworthy'][0]['url'];
}
}
class SourceRepository {
private $project;
private $data;
public function __construct($project, $data) {
$this->project = $project;
$this->data = $data;
}
/**
* Answer the project that owns the reciever.
*
* @return Project
*/
public function getProject() {
return $this->project;
}
public function getName() {
return $this->data['name'];
}
public function getPath() {
return $this->data['path'];
}
public function providesCommitActivity() {
return true;
}
public function isReal() {
// TODO test to see if the receiver represents a real repository.
return true;
}
/**
* Get the repository-technology specific URL for the repository.
* e.g. git://git.eclipse.org/gitroot/jgit/jgit.git
*/
public function getUrl() {
return $this->data['url'];
}
public function getType() {
return $this->data['type'];
}
/**
* Return an HTTP link for the receiver.
* i.e. something that is suitable
* for a browser
*/
public function getLink() {
return $this->getUrl();
}
}
/**
* Sort the array of projects by the given key.
* Valid keys
* are 'name', or 'top' (top level project).
*
* @param Project[] $projects
* @param string $key
*/
function sortProjects(&$projects, $key = 'name') {
if (!in_array($key, array(
'name',
'top'
)))
$key = 'name';
return usort($projects, "_sortProjects_$key");
}
/**
* Sort by Project name
*
* @internal
*
* @param Project $a
* @param Project $b
* @return number
*/
function _sortProjects_name($a, $b) {
return strcasecmp($a->getName(), $b->getName());
}
/**
* Sort project by Top Level Project.
*
* @internal
*
* @param Project $a
* @param Project $b
* @return number
*/
function _sortProjects_top($a, $b) {
return strcasecmp($a->getTopLevelProject()->getName(), $b->getTopLevelProject()->getName());
}
class ProjectBundlePatterns {
static $patterns;
public static function getAll() {
self::load();
return self::$patterns;
}
public function getPatterns($id) {
self::load();
return @self::$patterns[$id];
}
private static function load() {
if (self::$patterns !== null) return;
self::$patterns = array();
foreach(Project::getActiveProjects() as $project) {
$id = $project->getId();
if ($project->getShortId() == 'incubator') continue;
self::$patterns[$id] = array(
'forge' => $project->getForge()->getId(),
'id' => $project->getLocalId(),
'shortname' => $project->getShortId(),
'name' => $project->getName()
);
}
$mvnRegex = '([\w\-\*\.]+):([\w\-\*\.]+):([\w\-\*\.]+):([\w\-\*\.]+)';
if ($fp = fopen(dirname(__FILE__) . '/../ip-check/cq-map.txt', 'r')) {
while ($line = fgets($fp)) {
if (preg_match('/\s*([a-z][a-z0-9-_.]+),\s*(\(.*\))/', $line, $matches)) {
$id = $matches[1];
if (!isset(self::$patterns[$id])) continue;
$expression = $matches[2];
if (!in_array($expression,self::$patterns[$id]['expressions'])) {
self::$patterns[$id]['expressions'][] = $expression;
}
} elseif (preg_match("/^([a-z][a-z0-9-_.]+),\\s*$mvnRegex/", $line, $matches)) {
$id = $matches[1];
// Skip things that aren't projects.
if (!isset(self::$patterns[$id])) continue;
$groupid = $matches[2];
$artifactid = $matches[3];
$version = $matches[4] == 'jar' ? $matches[5] : $matches[4];
$maven = "$groupid:$artifactid:$version";
if (!in_array($maven, self::$patterns[$id]['maven'])) {
self::$patterns[$id]['maven'][] = $maven;
}
} elseif (preg_match('/^([a-z][a-z0-9-_.]+),\s*(.+\.jar)/i', $line, $matches)) {
$id = $matches[1];
$bundle = $matches[2];
if (!in_array($bundle, self::$patterns[$id]['bundles'])) {
self::$patterns[$id]['bundles'][] = $bundle;
}
}
}
fclose($fp);
}
}
}
// Initialize the class
Project::init();
?>