xref: /OpenGrok/opengrok-web/src/main/java/org/opengrok/web/ProjectHelper.java (revision c6f0939b1c668e9f8e1e276424439c3106b3a029)
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