1 /* 2 * CDDL HEADER START 3 * 4 * The contents of this file are subject to the terms of the 5 * Common Development and Distribution License (the "License"). 6 * You may not use this file except in compliance with the License. 7 * 8 * See LICENSE.txt included in this distribution for the specific 9 * language governing permissions and limitations under the License. 10 * 11 * When distributing Covered Code, include this CDDL HEADER in each 12 * file and include the License file at LICENSE.txt. 13 * If applicable, add the following below this CDDL HEADER, with the 14 * fields enclosed by brackets "[]" replaced with your own identifying 15 * information: Portions Copyright [yyyy] [name of copyright owner] 16 * 17 * CDDL HEADER END 18 */ 19 20 /* 21 * Copyright (c) 2015, 2021, Oracle and/or its affiliates. All rights reserved. 22 * Portions Copyright (c) 2018, Chris Fraire <cfraire@me.com>. 23 * Portions Copyright (c) 2019, Krystof Tulinger <k.tulinger@seznam.cz>. 24 */ 25 package org.opengrok.web; 26 27 import java.util.ArrayList; 28 import java.util.Comparator; 29 import java.util.List; 30 import java.util.Locale; 31 import java.util.Map; 32 import java.util.Set; 33 import java.util.TreeMap; 34 import java.util.TreeSet; 35 import java.util.stream.Collectors; 36 import org.opengrok.indexer.configuration.Group; 37 import org.opengrok.indexer.configuration.Project; 38 import org.opengrok.indexer.history.RepositoryInfo; 39 40 import static org.opengrok.web.PageConfig.OPEN_GROK_PROJECT; 41 42 /** 43 * Preprocessing of projects, repositories and groups for the UI. 44 * 45 * @author Krystof Tulinger 46 */ 47 public final class ProjectHelper { 48 49 private static final String ATTR_NAME = "project_helper"; 50 51 private static final String PROJECT_HELPER_GROUPS = "project_helper_groups"; 52 private static final String PROJECT_HELPER_UNGROUPED_PROJECTS = "project_helper_ungrouped_projects"; 53 private static final String PROJECT_HELPER_UNGROUPED_REPOSITORIES = "project_helper_ungrouped_repositories"; 54 private static final String PROJECT_HELPER_GROUPED_PROJECT_GROUP = "project_helper_grouped_project_group_"; 55 private static final String PROJECT_HELPER_GROUPED_REPOSITORIES = "project_helper_grouped_repositories"; 56 private static final String PROJECT_HELPER_ALLOWED_SUBGROUP = "project_helper_allowed_subgroup"; 57 private static final String PROJECT_HELPER_GROUPED_REPOSITORIES_GROUP = "project_helper_grouped_repositories_group_"; 58 private static final String PROJECT_HELPER_GROUPED_PROJECTS = "project_helper_grouped_projects"; 59 private static final String PROJECT_HELPER_SUBGROUPS_OF = "project_helper_subgroups_of_"; 60 private static final String PROJECT_HELPER_FAVOURITE_GROUP = "project_helper_favourite_group"; 61 62 private static final Comparator<RepositoryInfo> REPOSITORY_NAME_COMPARATOR = Comparator.comparing(RepositoryInfo::getDirectoryName); 63 64 private PageConfig cfg; 65 /** 66 * Set of groups. 67 */ 68 private final Set<Group> groups; 69 /** 70 * Set of projects (not repositories) without group. 71 */ 72 private final Set<Project> ungroupedProjects; 73 /** 74 * Set of all repositories without group. 75 */ 76 private final Set<Project> ungroupedRepositories; 77 /** 78 * Set of all projects with group. 79 */ 80 private final Set<Project> allProjects = new TreeSet<>(); 81 /** 82 * Set of all repositories with group. 83 */ 84 private final Set<Project> allRepositories = new TreeSet<>(); 85 ProjectHelper(PageConfig cfg)86 private ProjectHelper(PageConfig cfg) { 87 this.cfg = cfg; 88 groups = new TreeSet<>(cfg.getEnv().getGroups()); 89 ungroupedProjects = new TreeSet<>(); 90 ungroupedRepositories = new TreeSet<>(); 91 92 populateGroups(); 93 } 94 95 /** 96 * Object of project helper should be ONLY obtained by calling 97 * PageConfig#getProjectHelper. 98 * 99 * @param cfg current page config 100 * @return instance of ProjectHelper 101 * @see PageConfig#getProjectHelper() 102 */ getInstance(PageConfig cfg)103 public static ProjectHelper getInstance(PageConfig cfg) { 104 ProjectHelper instance = (ProjectHelper) cfg.getRequestAttribute(ATTR_NAME); 105 if (instance == null) { 106 instance = new ProjectHelper(cfg); 107 cfg.setRequestAttribute(ATTR_NAME, instance); 108 } 109 return instance; 110 } 111 112 /** 113 * Get repository info list for particular project. A copy of the list is 114 * returned always to allow concurrent modifications of the list in the 115 * caller. The items in the list shall not be modified concurrently, though. 116 * 117 * @param p the project for which we find the repository info list 118 * @return Copy of a list of repository info or empty list if no info is 119 * found 120 */ getRepositoryInfo(Project p)121 public List<RepositoryInfo> getRepositoryInfo(Project p) { 122 if (!cfg.isAllowed(p)) { 123 return new ArrayList<>(); 124 } 125 Map<Project, List<RepositoryInfo>> map = cfg.getEnv().getProjectRepositoriesMap(); 126 List<RepositoryInfo> info = map.get(p); 127 return info == null ? new ArrayList<>() : new ArrayList<>(info); 128 } 129 130 /** 131 * Get repository info list for particular project. A copy of the list is 132 * returned always to allow concurrent modifications of the list in the 133 * caller. The items in the list shall not be modified concurrently, though. 134 * This list is sorted with respect {@link #REPOSITORY_NAME_COMPARATOR}. 135 * 136 * @param p the project for which we find the repository info list 137 * @return Copy of a list of repository info or empty list if no info is 138 * found 139 */ getSortedRepositoryInfo(Project p)140 public List<RepositoryInfo> getSortedRepositoryInfo(Project p) { 141 return getRepositoryInfo(p) 142 .stream() 143 .sorted(REPOSITORY_NAME_COMPARATOR) 144 .collect(Collectors.toList()); 145 } 146 147 /** 148 * Generates ungrouped projects and repositories. 149 */ populateGroups()150 private void populateGroups() { 151 groups.addAll(cfg.getEnv().getGroups()); 152 for (Project project : cfg.getEnv().getProjectList()) { 153 // filterProjects() only adds groups which match project's name. 154 Set<Group> copy = Group.matching(project, groups); 155 156 // If no group matches the project, add it to not-grouped projects. 157 if (copy.isEmpty()) { 158 if (cfg.getEnv().getProjectRepositoriesMap().get(project) == null) { 159 ungroupedProjects.add(project); 160 } else { 161 ungroupedRepositories.add(project); 162 } 163 } 164 } 165 166 // populate all grouped 167 for (Group g : getGroups()) { 168 allProjects.addAll(g.getProjects()); 169 allRepositories.addAll(g.getRepositories()); 170 } 171 } 172 173 /** 174 * Filters set of projects based on the authorizer options 175 * and whether the project is indexed. 176 * 177 * @param p set of projects 178 * @return filtered set of projects 179 */ filterProjects(Set<Project> p)180 private Set<Project> filterProjects(Set<Project> p) { 181 Set<Project> repos = new TreeSet<>(p); 182 repos.removeIf(t -> !cfg.isAllowed(t) || !t.isIndexed()); 183 return repos; 184 } 185 186 /** 187 * Filters set of groups based on the authorizer options. 188 * 189 * @param p set of groups 190 * @return filtered set of groups 191 */ filterGroups(Set<Group> p)192 private Set<Group> filterGroups(Set<Group> p) { 193 Set<Group> grps = new TreeSet<>(p); 194 grps.removeIf(t -> !(cfg.isAllowed(t) || hasAllowedSubgroup(t))); 195 return grps; 196 } 197 198 /** 199 * Filters and saves the original set of projects into request's attribute. 200 * 201 * @param name attribute name 202 * @param original original set 203 * @return filtered set 204 */ 205 @SuppressWarnings(value = "unchecked") cacheProjects(String name, Set<Project> original)206 private Set<Project> cacheProjects(String name, Set<Project> original) { 207 Set<Project> p = (Set<Project>) cfg.getRequestAttribute(name); 208 if (p == null) { 209 p = filterProjects(original); 210 cfg.setRequestAttribute(name, p); 211 } 212 return p; 213 } 214 215 /** 216 * Filters and saves the original set of groups into request's attribute. 217 * 218 * @param name attribute name 219 * @param original original set 220 * @return filtered set 221 */ 222 @SuppressWarnings(value = "unchecked") cacheGroups(String name, Set<Group> original)223 private Set<Group> cacheGroups(String name, Set<Group> original) { 224 Set<Group> p = (Set<Group>) cfg.getRequestAttribute(name); 225 if (p == null) { 226 p = filterGroups(original); 227 cfg.setRequestAttribute(name, p); 228 } 229 return p; 230 } 231 232 /** 233 * @return filtered groups 234 */ getGroups()235 public Set<Group> getGroups() { 236 return cacheGroups(PROJECT_HELPER_GROUPS, groups); 237 } 238 239 /** 240 * @return filtered ungrouped projects 241 */ getProjects()242 public Set<Project> getProjects() { 243 return cacheProjects(PROJECT_HELPER_UNGROUPED_PROJECTS, ungroupedProjects); 244 } 245 246 /** 247 * @return filtered ungrouped repositories 248 */ getRepositories()249 public Set<Project> getRepositories() { 250 return cacheProjects(PROJECT_HELPER_UNGROUPED_REPOSITORIES, ungroupedRepositories); 251 } 252 253 /** 254 * @param g group 255 * @return filtered group's projects 256 */ getProjects(Group g)257 public Set<Project> getProjects(Group g) { 258 if (!cfg.isAllowed(g)) { 259 return new TreeSet<>(); 260 } 261 return cacheProjects(PROJECT_HELPER_GROUPED_PROJECT_GROUP + 262 g.getName().toLowerCase(Locale.ROOT), g.getProjects()); 263 } 264 265 /** 266 * @param g group 267 * @return filtered group's repositories 268 */ getRepositories(Group g)269 public Set<Project> getRepositories(Group g) { 270 if (!cfg.isAllowed(g)) { 271 return new TreeSet<>(); 272 } 273 return cacheProjects(PROJECT_HELPER_GROUPED_REPOSITORIES_GROUP + 274 g.getName().toLowerCase(Locale.ROOT), g.getRepositories()); 275 } 276 277 /** 278 * @return filtered grouped projects 279 */ getGroupedProjects()280 public Set<Project> getGroupedProjects() { 281 return cacheProjects(PROJECT_HELPER_GROUPED_PROJECTS, allProjects); 282 } 283 284 /** 285 * @return filtered grouped repositories 286 */ getGroupedRepositories()287 public Set<Project> getGroupedRepositories() { 288 return cacheProjects(PROJECT_HELPER_GROUPED_REPOSITORIES, allRepositories); 289 } 290 291 /** 292 * @see #getProjects() 293 * @return filtered ungrouped projects 294 */ getUngroupedProjects()295 public Set<Project> getUngroupedProjects() { 296 return cacheProjects(PROJECT_HELPER_UNGROUPED_PROJECTS, ungroupedProjects); 297 } 298 299 /** 300 * @see #getRepositories() 301 * @return filtered ungrouped projects 302 */ getUngroupedRepositories()303 public Set<Project> getUngroupedRepositories() { 304 return cacheProjects(PROJECT_HELPER_UNGROUPED_REPOSITORIES, ungroupedRepositories); 305 } 306 307 /** 308 * @return filtered projects and repositories 309 */ getAllGrouped()310 public Set<Project> getAllGrouped() { 311 return mergeProjects(getGroupedProjects(), getGroupedRepositories()); 312 } 313 314 /** 315 * @param g group 316 * @return filtered set of all projects and repositories in group g 317 */ getAllGrouped(Group g)318 public Set<Project> getAllGrouped(Group g) { 319 if (!cfg.isAllowed(g)) { 320 return new TreeSet<>(); 321 } 322 return mergeProjects(filterProjects(g.getProjects()), filterProjects(g.getRepositories())); 323 } 324 325 /** 326 * @return filtered set of all projects and repositories without group 327 */ getAllUngrouped()328 public Set<Project> getAllUngrouped() { 329 return mergeProjects(getUngroupedProjects(), getUngroupedRepositories()); 330 } 331 332 /** 333 * @return filtered set of all projects and repositories no matter if 334 * grouped or ungrouped 335 */ getAllProjects()336 public Set<Project> getAllProjects() { 337 return mergeProjects(getAllUngrouped(), getAllGrouped()); 338 } 339 340 /** 341 * @param g group 342 * @return filtered set of subgroups 343 */ getSubgroups(Group g)344 public Set<Group> getSubgroups(Group g) { 345 if (!cfg.isAllowed(g)) { 346 return new TreeSet<>(); 347 } 348 return cacheGroups(PROJECT_HELPER_SUBGROUPS_OF + 349 g.getName().toLowerCase(Locale.ROOT), g.getSubgroups()); 350 } 351 352 /** 353 * Checks if given group contains a subgroup which is allowed by the 354 * AuthorizationFramework. 355 * 356 * This should be used for deciding if this group should be written in the 357 * group hierarchy in the resulting html because it contains other allowed 358 * groups. 359 * 360 * @param group group 361 * @return true it it has an allowed subgroup 362 */ 363 @SuppressWarnings(value = "unchecked") hasAllowedSubgroup(Group group)364 public boolean hasAllowedSubgroup(Group group) { 365 Boolean val; 366 Map<String, Boolean> p = (Map<String, Boolean>) cfg.getRequestAttribute(PROJECT_HELPER_ALLOWED_SUBGROUP); 367 if (p == null) { 368 p = new TreeMap<>(); 369 cfg.setRequestAttribute(PROJECT_HELPER_ALLOWED_SUBGROUP, p); 370 } 371 val = p.get(group.getName()); 372 if (val == null) { 373 val = cfg.isAllowed(group); 374 val = val && !filterGroups(group.getDescendants()).isEmpty(); 375 p = (Map<String, Boolean>) cfg.getRequestAttribute(PROJECT_HELPER_ALLOWED_SUBGROUP); 376 p.put(group.getName(), val); 377 } 378 cfg.setRequestAttribute(PROJECT_HELPER_ALLOWED_SUBGROUP, p); 379 return val; 380 } 381 382 /** 383 * Checks if given group contains a favourite project. 384 * 385 * Favourite project is a project which is contained in the OpenGrokProject 386 * cookie, i. e. it has been searched or viewed by the user. 387 * 388 * This should by used to determine if this group should be displayed 389 * expanded or rolled up. 390 * 391 * @param group group 392 * @return true if it has favourite project 393 */ 394 @SuppressWarnings(value = "unchecked") hasFavourite(Group group)395 public boolean hasFavourite(Group group) { 396 Boolean val; 397 Map<String, Boolean> p = (Map<String, Boolean>) cfg.getRequestAttribute(PROJECT_HELPER_FAVOURITE_GROUP); 398 if (p == null) { 399 p = new TreeMap<>(); 400 cfg.setRequestAttribute(PROJECT_HELPER_FAVOURITE_GROUP, p); 401 } 402 val = p.get(group.getName()); 403 if (val == null) { 404 Set<Project> favourite = getAllGrouped(); 405 favourite.removeIf(t -> { 406 // project is favourite 407 if (!isFavourite(t)) { 408 return true; 409 } 410 // project is contained in group repositories 411 if (getRepositories(group).contains(t)) { 412 return false; 413 } 414 // project is contained in group projects 415 if (getProjects(group).contains(t)) { 416 return false; 417 } 418 // project is contained in subgroup's repositories and projects 419 for (Group g : filterGroups(group.getDescendants())) { 420 if (getProjects(g).contains(t)) { 421 return false; 422 } 423 if (getRepositories(g).contains(t)) { 424 return false; 425 } 426 } 427 return true; 428 }); 429 val = !favourite.isEmpty(); 430 p.put(group.getName(), val); 431 } 432 cfg.setRequestAttribute(PROJECT_HELPER_FAVOURITE_GROUP, p); 433 return val; 434 } 435 436 /** 437 * Checks if the project is a favourite project. 438 * 439 * @param project project 440 * @return true if it is favourite 441 */ isFavourite(Project project)442 public boolean isFavourite(Project project) { 443 return cfg.getCookieVals(OPEN_GROK_PROJECT).contains(project.getName()); 444 } 445 446 /** 447 * Checks if there is a favourite project in ungrouped projects. 448 * 449 * This should by used to determine if this 'other' section should be 450 * displayed expanded or rolled up. 451 * 452 * @return true if there is 453 */ hasUngroupedFavourite()454 public boolean hasUngroupedFavourite() { 455 for (Project p : getAllUngrouped()) { 456 if (isFavourite(p)) { 457 return true; 458 } 459 } 460 return false; 461 } 462 mergeProjects(Set<Project> p1, Set<Project> p2)463 private static Set<Project> mergeProjects(Set<Project> p1, Set<Project> p2) { 464 Set<Project> set = new TreeSet<>(); 465 set.addAll(p1); 466 set.addAll(p2); 467 return set; 468 } 469 cleanup(PageConfig cfg)470 public static void cleanup(PageConfig cfg) { 471 if (cfg != null) { 472 ProjectHelper helper = (ProjectHelper) cfg.getRequestAttribute(ATTR_NAME); 473 if (helper == null) { 474 return; 475 } 476 cfg.removeAttribute(ATTR_NAME); 477 helper.cfg = null; 478 } 479 } 480 } 481