blob: 7a874ca291a28c3feb8ade491a468eaf69c9cdbf [file] [log] [blame]
<?php
/*******************************************************************************
* Copyright (c) 2010 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");
/**
* Instances of the subclasses of Project represent a single Eclipse Foundation
* Project.
*
* 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.
*/
class Project {
static $config;
static $projects = array();
private $id;
private $data;
private $apiData = NULL;
/**
* 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 Project[]|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))"
));
}
public static function getAllWithShortname($name) {
if (!isValidProjectId($name)) return null;
return self::getProjects(array(
"(p.ProjectId like '%.{$name}')",
"p.IsActive"
));
}
public static function getTopLevelProjects() {
$where = array(
"p.IsActive",
"(p.ProjectID not in ('root', 'foundation-internal', 'galileo'))",
"(p.ParentProjectID not in ('foundation-internal'))",
"p.ParentProjectID = 'root'"
);
return self::getProjects($where);
}
public static function getActiveProjects() {
$where = array(
"p.IsActive",
"(p.ProjectID not in ('root', 'foundation-internal', 'galileo'))",
"(p.ParentProjectID not in ('foundation-internal'))"
);
return self::getProjects($where);
}
public static function getProjectsForCommitter($id) {
$where = array(
"(p.ProjectID in (SELECT ProjectID FROM PeopleProjects where PersonId='$id' and Relation='CM' and InactiveDate is null))",
"p.IsActive"
);
return self::getProjects($where);
}
public static function getSubprojects($id) {
return self::getProjects(array(
"(p.ParentProjectId = '$id')",
"p.IsActive"
));
}
public static function visit($callable, $pre = null, $post = null) {
if ($pre) call_user_func($pre, null);
foreach (self::getTopLevelProjects() as $project) {
$project->visitHierarchy($callable, $pre, $post, 0);
}
if ($post) call_user_func($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 Project($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;
}
public function __construct($id, $data) {
$this->id = $id;
$this->data = $data;
}
/**
* 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() {
$matches = null;
preg_match("/([^\.]+)$/", $this->getId(), $matches);
return $matches[1];
}
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->getId();
return "https://projects.eclipse.org/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() {
return 'https://projects.eclipse.org/api/projects/' . preg_replace('/\./','_',$this->getId());
}
public function getTopLevelProject() {
if ($this->isTopLevel())
return $this;
if (!$parent = $this->getParent())
return $this;
return $parent->getTopLevelProject();
}
public function isTopLevel() {
return $this->getParentId() == 'root';
}
public function visitHierarchy($function, $pre = null, $post = null, $level) {
call_user_func($function, $this, $level);
if ($pre) call_user_func($pre, $this);
foreach ($this->getChildren() as $child) {
$child->visitHierarchy($function, $pre, $post, $level + 1);
}
if ($post) call_user_func($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';
}
/**
* Answer an array containing information about the source
* repositories registered for the project.
*/
public function getSourceRepositories() {
$project = $this;
$repositories = array();
$sql = "select path from GitRepo where project=':id:'";
query('dashboard', $sql, array(':id:' => $this->getId()), function($row) use (&$project, &$repositories) {
$repositories[] = $row['path'];
});
return $repositories;
}
/**
* Answers an array containing the trademarks that are known
* to be associated with the project.
*
* @return mixed
*/
function getTrademarks() {
$sql = "select name, demarcation, type from Trademark where category='project' and type is not null and id=':id:'";
$trademarks = array();
query('dashboard', $sql, array(':id:' => $this->getId()), function($row) use (&$trademarks) {
$trademarks[] = $row;
});
return $trademarks;
}
public function __toString() {
return "Project ({$this->getId()})";
}
public function getId() {
return $this->id;
}
public function getParentId() {
return $this->data['parent'];
}
public function getName() {
$name = $this->data ['name'];
$name = preg_replace('/\s*\[archived\]\s*/','', $name);
$name = preg_replace ( '/ Root$/', '', $name);
return $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 getDescription() {
$apiData = $this->getAPIData();
return @$apiData[0]['description'];
}
public function getSpecificationWorkingGroupName() {
$apiData = $this->getAPIData();
return @$apiData[0]['spec_project_working_group'][0]['name'];
}
public function isSpecificationProject() {
$apiData = $this->getAPIData();
return !empty($apiData[0]['spec_project_working_group']);
}
public function getDevListUrl() {
$apiData = $this->getAPIData();
return @$apiData[0]['dev_list']['url'];
}
/**
* Get the data related to this project from the Eclipse API.
*
* @return array|mixed
*/
private function getAPIData() {
if ($this->apiData === NULL) {
$url = $this->getDataUrl();
$contents = getUrlContents($url);
$this->apiData = json_decode($contents, true);
// If we get empty data, make sure that it's not NULL, so that we
// don't try again.
if (!$this->apiData) $this->apiData = array();
}
return $this->apiData;
}
}
/**
* 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());
}
// Initialize the class
Project::init();
?>