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