| <?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(); |
| ?> |