xref: /OpenGrok/opengrok-indexer/src/main/java/org/opengrok/indexer/configuration/Project.java (revision 76ceaf0eb6c53d5570ac2c2b2575263528a63eae)
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) 2006, 2022, Oracle and/or its affiliates. All rights reserved.
22  * Portions Copyright (c) 2018, Chris Fraire <cfraire@me.com>.
23  */
24 package org.opengrok.indexer.configuration;
25 
26 import java.io.File;
27 import java.io.FileNotFoundException;
28 import java.io.IOException;
29 import java.io.Serializable;
30 import java.util.Locale;
31 import java.util.Set;
32 import java.util.TreeSet;
33 import java.util.logging.Level;
34 import java.util.logging.Logger;
35 import java.util.regex.PatternSyntaxException;
36 
37 import org.jetbrains.annotations.VisibleForTesting;
38 import org.opengrok.indexer.logger.LoggerFactory;
39 import org.opengrok.indexer.util.ClassUtil;
40 import org.opengrok.indexer.util.ForbiddenSymlinkException;
41 import org.opengrok.indexer.web.Util;
42 
43 import static org.opengrok.indexer.configuration.PatternUtil.compilePattern;
44 
45 /**
46  * Placeholder for the information that builds up a project.
47  */
48 public class Project implements Comparable<Project>, Nameable, Serializable {
49 
50     private static final long serialVersionUID = 1L;
51 
52     private static final Logger LOGGER = LoggerFactory.getLogger(Project.class);
53 
54     static {
55         ClassUtil.remarkTransientFields(Project.class);
56     }
57 
58     /**
59      * Path relative to source root. Uses the '/' separator on all platforms.
60      */
61     private String path;
62 
63     /**
64      * This variable is very important, since it's used as the project
65      * identifier all over xrefs and webapp.
66      */
67     private String name;
68 
69     /**
70      * Size of tabs in this project. Used for displaying the xrefs correctly in
71      * projects with non-standard tab size.
72      */
73     private int tabSize;
74 
75     /**
76      * A flag if the navigate window should be opened by default when browsing
77      * the source code of this project.
78      */
79     private Boolean navigateWindowEnabled = null;
80 
81     /**
82      * This flag sets per-project handling of renamed files.
83      */
84     private Boolean handleRenamedFiles = null;
85 
86     /**
87      * This flag enables/disables per-project history cache.
88      */
89     private Boolean historyEnabled = null;
90 
91     /**
92      * This flag enables/disables per project merge commits.
93      */
94     private Boolean mergeCommitsEnabled = null;
95 
96     /**
97      * This marks the project as (not)ready before initial index is done. this
98      * is to avoid all/multi-project searches referencing this project from
99      * failing.
100      */
101     private boolean indexed = false;
102 
103     /**
104      * This flag sets per-project reindex based on traversing SCM history.
105      */
106     private Boolean historyBasedReindex = null;
107 
108     /**
109      * Set of groups which match this project.
110      */
111     private transient Set<Group> groups = new TreeSet<>();
112 
113     /**
114      * These properties override global settings, if set.
115      */
116     private String bugPage;
117     private String bugPattern;
118     private String reviewPage;
119     private String reviewPattern;
120 
121     // This empty constructor is needed for serialization.
Project()122     public Project() {
123     }
124 
125     /**
126      * Create a project with given name.
127      *
128      * @param name the name of the project
129      */
Project(String name)130     public Project(String name) {
131         this.name = name;
132     }
133 
134     /**
135      * Create a project with given name and path and default configuration
136      * values.
137      *
138      * @param name the name of the project
139      * @param path the path of the project relative to the source root
140      */
Project(String name, String path)141     public Project(String name, String path) {
142         this.name = name;
143         this.path = Util.fixPathIfWindows(path);
144         completeWithDefaults();
145     }
146 
147     /**
148      * Get a textual name of this project.
149      *
150      * @return a textual name of the project
151      */
152     @Override
getName()153     public String getName() {
154         return name;
155     }
156 
157     /**
158      * Get the path (relative from source root) where this project is located.
159      *
160      * @return the relative path
161      */
getPath()162     public String getPath() {
163         return path;
164     }
165 
isIndexed()166     public boolean isIndexed() {
167         return indexed;
168     }
169 
170     /**
171      * Get the project id.
172      *
173      * @return the id of the project
174      */
getId()175     public String getId() {
176         return path;
177     }
178 
179     /**
180      * Get the tab size for this project, if tab size has been set.
181      *
182      * @return tab size if set, 0 otherwise
183      * @see #hasTabSizeSetting()
184      */
getTabSize()185     public int getTabSize() {
186         return tabSize;
187     }
188 
189     /**
190      * Set a textual name of this project, preferably don't use " , " in the
191      * name, since it's used as delimiter for more projects
192      *
193      * XXX we should not allow setting project name after it has been
194      * constructed because it is probably part of HashMap.
195      *
196      * @param name a textual name of the project
197      */
198     @Override
setName(String name)199     public void setName(String name) {
200         this.name = name;
201     }
202 
203     /**
204      * Set the path (relative from source root) this project is located.
205      *
206      * @param path the relative path from source root where this project is
207      * located, starting with path separator.
208      */
setPath(String path)209     public void setPath(String path) {
210         this.path = Util.fixPathIfWindows(path);
211     }
212 
setIndexed(boolean flag)213     public void setIndexed(boolean flag) {
214         this.indexed = flag;
215     }
216 
217     /**
218      * Set tab size for this project. Used for expanding tabs to spaces in
219      * xrefs.
220      *
221      * @param tabSize the size of tabs in this project
222      */
setTabSize(int tabSize)223     public void setTabSize(int tabSize) {
224         this.tabSize = tabSize;
225     }
226 
227     /**
228      * Has this project an explicit tab size setting?
229      *
230      * @return {@code true} if the tab size has been set for this project, or
231      * {@code false} if it hasn't and the default should be used
232      */
hasTabSizeSetting()233     public boolean hasTabSizeSetting() {
234         return tabSize > 0;
235     }
236 
237     /**
238      * Indicate whether the navigate window should be opened by default when
239      * browsing a source code from this project.
240      *
241      * @return true if yes; false otherwise
242      */
isNavigateWindowEnabled()243     public boolean isNavigateWindowEnabled() {
244         return navigateWindowEnabled != null && navigateWindowEnabled;
245     }
246 
247     /**
248      * Set the value of navigateWindowEnabled.
249      *
250      * @param navigateWindowEnabled new value of navigateWindowEnabled
251      */
setNavigateWindowEnabled(boolean navigateWindowEnabled)252     public void setNavigateWindowEnabled(boolean navigateWindowEnabled) {
253         this.navigateWindowEnabled = navigateWindowEnabled;
254     }
255 
256     /**
257      * @return true if this project handles renamed files.
258      */
isHandleRenamedFiles()259     public boolean isHandleRenamedFiles() {
260         return handleRenamedFiles != null && handleRenamedFiles;
261     }
262 
263     /**
264      * @return true if merge commits are enabled.
265      */
isMergeCommitsEnabled()266     public boolean isMergeCommitsEnabled() {
267         return mergeCommitsEnabled != null && mergeCommitsEnabled;
268     }
269 
270     /**
271      * @param flag true if project should handle renamed files, false otherwise.
272      */
setHandleRenamedFiles(boolean flag)273     public void setHandleRenamedFiles(boolean flag) {
274         this.handleRenamedFiles = flag;
275     }
276 
277     /**
278      * @return true if this project should have history cache.
279      */
isHistoryEnabled()280     public boolean isHistoryEnabled() {
281         return historyEnabled != null && historyEnabled;
282     }
283 
284     /**
285      * @param flag true if project should have history cache, false otherwise.
286      */
setHistoryEnabled(boolean flag)287     public void setHistoryEnabled(boolean flag) {
288         this.historyEnabled = flag;
289     }
290 
291     /**
292      * @param flag true if project's repositories should deal with merge commits.
293      */
setMergeCommitsEnabled(boolean flag)294     public void setMergeCommitsEnabled(boolean flag) {
295         this.mergeCommitsEnabled = flag;
296     }
297 
298     /**
299      * @return true if this project handles renamed files.
300      */
isHistoryBasedReindex()301     public boolean isHistoryBasedReindex() {
302         return historyBasedReindex != null && historyBasedReindex;
303     }
304 
305     /**
306      * @param flag true if project should handle renamed files, false otherwise.
307      */
setHistoryBasedReindex(boolean flag)308     public void setHistoryBasedReindex(boolean flag) {
309         this.historyBasedReindex = flag;
310     }
311 
312     @VisibleForTesting
clearProperties()313     public void clearProperties() {
314         historyBasedReindex = null;
315         mergeCommitsEnabled = null;
316         historyEnabled = null;
317         handleRenamedFiles = null;
318     }
319 
320     /**
321      * Return groups where this project belongs.
322      *
323      * @return set of groups|empty if none
324      */
getGroups()325     public Set<Group> getGroups() {
326         return groups;
327     }
328 
setGroups(Set<Group> groups)329     public void setGroups(Set<Group> groups) {
330         this.groups = groups;
331     }
332 
333     /**
334      * Adds a group where this project belongs.
335      *
336      * @param group group to add
337      */
addGroup(Group group)338     public void addGroup(Group group) {
339         while (group != null) {
340             this.groups.add(group);
341             group = group.getParent();
342         }
343     }
344 
setBugPage(String bugPage)345     public void setBugPage(String bugPage) {
346         this.bugPage = bugPage;
347     }
348 
getBugPage()349     public String getBugPage() {
350         if (bugPage != null) {
351             return bugPage;
352         } else {
353             return RuntimeEnvironment.getInstance().getBugPage();
354         }
355     }
356 
357     /**
358      * Set the bug pattern to a new value.
359      *
360      * @param bugPattern the new pattern
361      * @throws PatternSyntaxException when the pattern is not a valid regexp or
362      * does not contain at least one capture group and the group does not
363      * contain a single character
364      */
setBugPattern(String bugPattern)365     public void setBugPattern(String bugPattern) throws PatternSyntaxException {
366         this.bugPattern = compilePattern(bugPattern);
367     }
368 
getBugPattern()369     public String getBugPattern() {
370         if (bugPattern != null) {
371             return bugPattern;
372         } else {
373             return RuntimeEnvironment.getInstance().getBugPattern();
374         }
375     }
376 
getReviewPage()377     public String getReviewPage() {
378         if (reviewPage != null) {
379             return reviewPage;
380         } else {
381             return RuntimeEnvironment.getInstance().getReviewPage();
382         }
383     }
384 
setReviewPage(String reviewPage)385     public void setReviewPage(String reviewPage) {
386         this.reviewPage = reviewPage;
387     }
388 
getReviewPattern()389     public String getReviewPattern() {
390         if (reviewPattern != null) {
391             return reviewPattern;
392         } else {
393             return RuntimeEnvironment.getInstance().getReviewPattern();
394         }
395     }
396 
397     /**
398      * Set the review pattern to a new value.
399      *
400      * @param reviewPattern the new pattern
401      * @throws PatternSyntaxException when the pattern is not a valid regexp or
402      * does not contain at least one capture group and the group does not
403      * contain a single character
404      */
setReviewPattern(String reviewPattern)405     public void setReviewPattern(String reviewPattern) throws PatternSyntaxException {
406         this.reviewPattern = compilePattern(reviewPattern);
407     }
408 
409     /**
410      * Fill the project with the current configuration where the applicable
411      * project property has a default value.
412      */
completeWithDefaults()413     public final void completeWithDefaults() {
414         Configuration defaultCfg = new Configuration();
415         final RuntimeEnvironment env = RuntimeEnvironment.getInstance();
416 
417         /*
418          * Choosing strategy for properties (tabSize used as example here):
419          * <pre>
420          * this       cfg        defaultCfg   chosen value
421          * ===============================================
422          *  |5|        4             0             5
423          *   0        |4|            0             4
424          * </pre>
425          *
426          * The strategy is:
427          * 1) if the project has some non-default value; use that
428          * 2) if the project has a default value; use the provided configuration
429          */
430         if (getTabSize() == defaultCfg.getTabSize()) {
431             setTabSize(env.getTabSize());
432         }
433 
434         // Allow project to override global setting of renamed file handling.
435         if (handleRenamedFiles == null) {
436             setHandleRenamedFiles(env.isHandleHistoryOfRenamedFiles());
437         }
438 
439         // Allow project to override global setting of history cache generation.
440         if (historyEnabled == null) {
441             setHistoryEnabled(env.isHistoryEnabled());
442         }
443 
444         // Allow project to override global setting of navigate window.
445         if (navigateWindowEnabled == null) {
446             setNavigateWindowEnabled(env.isNavigateWindowEnabled());
447         }
448 
449         // Allow project to override global setting of merge commits.
450         if (mergeCommitsEnabled == null) {
451             setMergeCommitsEnabled(env.isMergeCommitsEnabled());
452         }
453 
454         if (bugPage == null) {
455             setBugPage(env.getBugPage());
456         }
457         if (bugPattern == null) {
458             setBugPattern(env.getBugPattern());
459         }
460 
461         if (reviewPage == null) {
462             setReviewPage(env.getReviewPage());
463         }
464         if (reviewPattern == null) {
465             setReviewPattern(env.getReviewPattern());
466         }
467 
468         if (historyBasedReindex == null) {
469             setHistoryBasedReindex(env.isHistoryBasedReindex());
470         }
471     }
472 
473     /**
474      * Get the project for a specific file.
475      *
476      * @param path the file to lookup (relative to source root)
477      * @return the project that this file belongs to (or null if the file
478      * doesn't belong to a project)
479      */
getProject(String path)480     public static Project getProject(String path) {
481         // Try to match each project path as prefix of the given path.
482         final RuntimeEnvironment env = RuntimeEnvironment.getInstance();
483         if (env.hasProjects()) {
484             final String lpath = Util.fixPathIfWindows(path);
485             for (Project p : env.getProjectList()) {
486                 String projectPath = p.getPath();
487                 if (projectPath == null) {
488                     LOGGER.log(Level.WARNING, "Path of project {0} is not set", p.getName());
489                     return null;
490                 }
491 
492                 // Check if the project's path is a prefix of the given
493                 // path. It has to be an exact match, or the project's path
494                 // must be immediately followed by a separator. "/foo" is
495                 // a prefix for "/foo" and "/foo/bar", but not for "/foof".
496                 if (lpath.startsWith(projectPath)
497                         && (projectPath.length() == lpath.length()
498                         || lpath.charAt(projectPath.length()) == '/')) {
499                     return p;
500                 }
501             }
502         }
503 
504         return null;
505     }
506 
507     /**
508      * Get the project for a specific file.
509      *
510      * @param file the file to lookup
511      * @return the project that this file belongs to (or {@code null} if the file doesn't belong to a project)
512      */
getProject(File file)513     public static Project getProject(File file) {
514         Project ret = null;
515         try {
516             ret = getProject(RuntimeEnvironment.getInstance().getPathRelativeToSourceRoot(file));
517         } catch (FileNotFoundException e) { // NOPMD
518             // ignore if not under source root
519         } catch (ForbiddenSymlinkException e) {
520             LOGGER.log(Level.FINER, e.getMessage());
521             // ignore
522         } catch (IOException e) { // NOPMD
523             // problem has already been logged, just return null
524         }
525         return ret;
526     }
527 
528     /**
529      * Returns project object by its name, used in webapp to figure out which
530      * project is to be searched.
531      *
532      * @param name name of the project
533      * @return project that fits the name
534      */
getByName(String name)535     public static Project getByName(String name) {
536         RuntimeEnvironment env = RuntimeEnvironment.getInstance();
537         if (env.hasProjects()) {
538             Project proj;
539             if ((proj = env.getProjects().get(name)) != null) {
540                 return (proj);
541             }
542         }
543         return null;
544     }
545 
546     @Override
compareTo(Project p2)547     public int compareTo(Project p2) {
548         return getName().toUpperCase(Locale.ROOT).compareTo(
549                 p2.getName().toUpperCase(Locale.ROOT));
550     }
551 
552     @Override
hashCode()553     public int hashCode() {
554         int hash = 3;
555         hash = 41 * hash + (this.name == null ? 0 :
556                 this.name.toUpperCase(Locale.ROOT).hashCode());
557         return hash;
558     }
559 
560     @Override
equals(Object obj)561     public boolean equals(Object obj) {
562         if (this == obj) {
563             return true;
564         }
565         if (obj == null) {
566             return false;
567         }
568         if (getClass() != obj.getClass()) {
569             return false;
570         }
571         final Project other = (Project) obj;
572 
573         int numNull = (name == null ? 1 : 0) + (other.name == null ? 1 : 0);
574         switch (numNull) {
575             case 0:
576                 return name.toUpperCase(Locale.ROOT).equals(
577                         other.name.toUpperCase(Locale.ROOT));
578             case 1:
579                 return false;
580             default:
581                 return true;
582         }
583     }
584 
585     @Override
toString()586     public String toString() {
587         return getName() + ":indexed=" + isIndexed() + ",history=" + isHistoryEnabled();
588     }
589 }
590