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