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) 2016, 2021, Oracle and/or its affiliates. All rights reserved. 22 */ 23 package org.opengrok.indexer.configuration; 24 25 import java.io.File; 26 import java.io.FileNotFoundException; 27 import java.io.IOException; 28 import java.io.PrintStream; 29 import java.nio.charset.StandardCharsets; 30 import java.text.ParseException; 31 import java.util.ArrayList; 32 import java.util.LinkedList; 33 import java.util.List; 34 import java.util.Set; 35 import org.opengrok.indexer.util.Getopt; 36 37 /** 38 * 39 * @author Krystof Tulinger 40 */ 41 public final class Groups { 42 43 /** 44 * Interface used to perform an action to a single group. 45 */ 46 private interface Walker { 47 48 /** 49 * @param g group 50 * @return true if traversing should stop just after this group, false 51 * otherwise 52 */ call(Group g)53 boolean call(Group g); 54 } 55 Groups()56 private Groups() { 57 } 58 main(String[] argv)59 public static void main(String[] argv) { 60 PrintStream out = System.out; 61 File outFile = null; 62 Configuration cfg = new Configuration(); 63 String groupname = null; 64 String grouppattern = null; 65 String parent = null; 66 boolean list = false; 67 boolean delete = false; 68 boolean empty = false; 69 String match = null; 70 71 Getopt getopt = new Getopt(argv, "dehi:lm:n:o:p:r:?"); 72 73 try { 74 getopt.parse(); 75 } catch (ParseException ex) { 76 System.err.println("Groups: " + ex.getMessage()); 77 usage(System.err); 78 System.exit(1); 79 } 80 81 try { 82 int cmd; 83 File f; 84 getopt.reset(); 85 while ((cmd = getopt.getOpt()) != -1) { 86 switch (cmd) { 87 case 'd': 88 delete = true; 89 break; 90 case 'e': 91 empty = true; 92 break; 93 case 'h': 94 usage(System.out); 95 System.exit(0); 96 break; 97 case 'i': 98 f = new File(getopt.getOptarg()); 99 try { 100 cfg = Configuration.read(f); 101 } catch (ArrayIndexOutOfBoundsException ex) { 102 System.err.println("An error occurred - this may mean that the input file is not well-formated."); 103 System.err.println(); 104 ex.printStackTrace(System.err); 105 System.exit(3); 106 } 107 break; 108 case 'l': 109 list = true; 110 break; 111 case 'm': 112 match = getopt.getOptarg(); 113 break; 114 case 'n': 115 groupname = getopt.getOptarg(); 116 break; 117 case 'o': 118 outFile = new File(getopt.getOptarg()); 119 break; 120 case 'p': 121 parent = getopt.getOptarg(); 122 break; 123 case 'r': 124 grouppattern = getopt.getOptarg(); 125 break; 126 case '?': 127 usage(System.out); 128 System.exit(0); 129 break; 130 default: 131 System.err.println("Internal Error - Not implemented option: " + (char) cmd); 132 usage(System.err); 133 System.exit(1); 134 break; 135 } 136 } 137 } catch (FileNotFoundException ex) { 138 System.err.println("An error occurred - file does not exist"); 139 ex.printStackTrace(System.err); 140 System.exit(3); 141 } catch (IOException ex) { 142 System.err.println("An unknown error occurred - the input file may be corrupted"); 143 ex.printStackTrace(System.err); 144 System.exit(3); 145 } 146 147 if (match != null) { 148 // perform matching 149 if (parent != null || groupname != null || grouppattern != null) { 150 System.err.println("Match option should be used without parent|groupname|groupregex options"); 151 usage(System.err); 152 System.exit(1); 153 } 154 matchGroups(System.out, cfg.getGroups(), match); 155 } else if (empty) { 156 // just list the groups 157 if (parent != null || groupname != null || grouppattern != null) { 158 System.err.println("Match option should be used without parent|groupname|groupregex options"); 159 usage(System.err); 160 System.exit(1); 161 } 162 out = prepareOutput(outFile); 163 printOut(false, cfg, out); 164 } else if (delete) { 165 // perform delete 166 if (parent != null || grouppattern != null) { 167 System.err.println("Delete option should be used without parent|groupregex options"); 168 usage(System.err); 169 System.exit(1); 170 } 171 if (groupname == null) { 172 System.err.println("You must specify the group name"); 173 usage(System.err); 174 System.exit(1); 175 } 176 deleteGroup(cfg.getGroups(), groupname); 177 out = prepareOutput(outFile); 178 printOut(list, cfg, out); 179 } else if (groupname != null) { 180 if (grouppattern == null) { 181 grouppattern = ""; 182 } 183 // perform insert/update. parent may be null 184 if (!modifyGroup(cfg.getGroups(), groupname, grouppattern, parent)) { 185 System.err.println("Parent group does not exist \"" + parent + "\""); 186 } else { 187 out = prepareOutput(outFile); 188 printOut(list, cfg, out); 189 } 190 } else if (list) { 191 // just list the groups 192 if (groupname != null) { 193 System.err.println("List option should be used without groupname options"); 194 usage(System.err); 195 System.exit(1); 196 } 197 printOut(list, cfg, out); 198 } else { 199 System.err.println("Wrong combination of options. See usage."); 200 usage(System.err); 201 System.exit(2); 202 } 203 } 204 205 /** 206 * Prints the configuration to the stream. 207 * 208 * @param list if true then it lists all available groups in configuration 209 * if @param out is different than stdout it also prints the current 210 * configuration to that stream otherwise it prints the configuration to the 211 * @param out stream. 212 * @param cfg configuration 213 * @param out output stream 214 */ printOut(boolean list, Configuration cfg, PrintStream out)215 private static void printOut(boolean list, Configuration cfg, PrintStream out) { 216 if (list) { 217 listGroups(System.out, cfg.getGroups()); 218 if (out != System.out) { 219 out.print(cfg.getXMLRepresentationAsString()); 220 } 221 } else { 222 out.print(cfg.getXMLRepresentationAsString()); 223 } 224 } 225 prepareOutput(File outFile)226 private static PrintStream prepareOutput(File outFile) { 227 PrintStream out = System.out; 228 if (outFile != null) { 229 try { 230 out = new PrintStream(outFile, StandardCharsets.UTF_8); 231 } catch (IOException ex) { 232 System.err.println("An error occurred - " + ex.getMessage()); 233 ex.printStackTrace(System.err); 234 System.exit(3); 235 } 236 } 237 return out; 238 } 239 240 /** 241 * List groups given as a parameter. 242 * 243 * @param out stream 244 * @param groups groups 245 */ listGroups(PrintStream out, Set<Group> groups)246 private static void listGroups(PrintStream out, Set<Group> groups) { 247 treeTraverseGroups(groups, g -> { 248 for (int i = 0; i < g.getFlag() * 2; i++) { 249 out.print(" "); 250 } 251 out.println(g.getName() + " ~ '" + g.getPattern() + "'"); 252 return false; 253 }); 254 } 255 256 /** 257 * Finds groups which would match the project. 258 * 259 * @param out stream to write the results 260 * @param groups set of groups 261 * @param match project name 262 */ matchGroups(PrintStream out, Set<Group> groups, String match)263 private static void matchGroups(PrintStream out, Set<Group> groups, String match) { 264 Project p = new Project(match); 265 266 List<Group> matched = new ArrayList<>(); 267 linearTraverseGroups(groups, g -> { 268 if (g.match(p)) { 269 matched.add(g); 270 } 271 return false; 272 }); 273 274 out.println(matched.size() + " group(s) match(es) the \"" + match + "\""); 275 for (Group g : matched) { 276 out.println(g.getName() + " '" + g.getPattern() + "'"); 277 } 278 } 279 280 /** 281 * Adds a group into the xml tree. 282 * 283 * If group already exists, only the pattern is modified. Parent group can 284 * be null, in that case a new group is inserted as a top level group. 285 * 286 * @param groups existing groups 287 * @param groupname new group name 288 * @param grouppattern new group pattern 289 * @param parent parent 290 * @return false if parent group was not found, true otherwise 291 */ modifyGroup(Set<Group> groups, String groupname, String grouppattern, String parent)292 private static boolean modifyGroup(Set<Group> groups, String groupname, String grouppattern, String parent) { 293 Group g = new Group(groupname, grouppattern); 294 295 if (updateGroup(groups, groupname, grouppattern)) { 296 return true; 297 } 298 299 if (parent != null) { 300 if (insertToParent(groups, parent, g)) { 301 groups.add(g); 302 return true; 303 } 304 return false; 305 } 306 307 groups.add(g); 308 return true; 309 } 310 311 /** 312 * Removes group from the xml tree. 313 * 314 * @param groups existing groups 315 * @param groupname group to remove 316 */ deleteGroup(Set<Group> groups, String groupname)317 private static void deleteGroup(Set<Group> groups, String groupname) { 318 for (Group g : groups) { 319 if (g.getName().equals(groupname)) { 320 groups.remove(g); 321 groups.removeAll(g.getDescendants()); 322 return; 323 } 324 } 325 } 326 327 /** 328 * Traverse the set of groups starting in top level groups (groups without a 329 * parent group) and performing depth first search in the group's subgroups. 330 * 331 * @param groups set of groups (mixed top level and other groups) 332 * @param walker an instance of {@link Walker} which is used for every 333 * traversed group 334 * @return true if {@code walker} emits true for any of the groups; false 335 * otherwise 336 * 337 * @see Walker 338 */ treeTraverseGroups(Set<Group> groups, Walker walker)339 private static boolean treeTraverseGroups(Set<Group> groups, Walker walker) { 340 LinkedList<Group> stack = new LinkedList<>(); 341 for (Group g : groups) { 342 // the flag here represents the group's depth in the group tree 343 g.setFlag(0); 344 if (g.getParent() == null) { 345 stack.addLast(g); 346 } 347 } 348 349 while (!stack.isEmpty()) { 350 Group g = stack.getFirst(); 351 stack.removeFirst(); 352 353 if (walker.call(g)) { 354 return true; 355 } 356 357 g.getSubgroups().forEach(x -> x.setFlag(g.getFlag() + 1)); 358 // add all the subgroups respecting the sorted order 359 stack.addAll(0, g.getSubgroups()); 360 } 361 return false; 362 } 363 364 /** 365 * Traverse the set of groups linearly based on the set's iterator. 366 * 367 * @param groups set of groups (mixed top level and other groups) 368 * @param walker an instance of {@link Walker} which is used for every 369 * traversed group 370 * @return true if {@code walker} emits true for any of the groups; false 371 */ linearTraverseGroups(Set<Group> groups, Walker walker)372 private static boolean linearTraverseGroups(Set<Group> groups, Walker walker) { 373 for (Group g : groups) { 374 if (walker.call(g)) { 375 return true; 376 } 377 } 378 return false; 379 } 380 insertToParent(Set<Group> groups, String parent, Group g)381 private static boolean insertToParent(Set<Group> groups, String parent, Group g) { 382 return linearTraverseGroups(groups, x -> { 383 if (x.getName().equals(parent)) { 384 x.addGroup(g); 385 Group tmp = x.getParent(); 386 while (tmp != null) { 387 tmp.addDescendant(g); 388 tmp = tmp.getParent(); 389 } 390 return true; 391 } 392 return false; 393 }); 394 } 395 396 private static boolean updateGroup(Set<Group> groups, String groupname, String grouppattern) { 397 return linearTraverseGroups(groups, g -> { 398 if (g.getName().equals(groupname)) { 399 g.setPattern(grouppattern); 400 return true; 401 } 402 return false; 403 }); 404 } 405 406 private static void usage(PrintStream out) { 407 out.println("Usage:"); 408 out.println("Groups.java" + " [OPTIONS]"); 409 out.println(); 410 out.println("OPTIONS:"); 411 out.println("Help"); 412 out.println("-? print this help message"); 413 out.println("-h print this help message"); 414 out.println("-v verbose/debug mode"); 415 out.println(); 416 out.println("Input/Output"); 417 out.println("-i /path/to/file input file|default is empty configuration"); 418 out.println("-o /path/to/file output file|default is stdout"); 419 out.println(); 420 out.println("Listing"); 421 out.println("-m <project name> performs matching based on given project name"); 422 out.println("-l lists all available groups in input file"); 423 out.println("-e creates an empty configuration or"); 424 out.println(" directly outputs the input file (if given)"); 425 out.println(); 426 out.println("Modification"); 427 out.println("-n <group name> specify group name which should be inserted|updated (requires either -r or -d option)"); 428 out.println("-r <group regex> specify group regex pattern (requires -n option)"); 429 out.println("-p <parent group> optional parameter for the parent group name (requires -n option)"); 430 out.println("-d delete specified group (requires -n option)"); 431 out.println(); 432 out.println("NOTE: using modification options with -l forces the program to list all\n" 433 + "available groups after the modification. Output to a file can still be used."); 434 out.println(); 435 out.println("Examples"); 436 out.println("-i ~/c.xml -l # => list groups"); 437 out.println("-n Abcd -r \"abcd.*\" -o ~/c.xml # => add group and print to ~/c.xml"); 438 out.println("-i ~/c.xml -m abcdefg # => prints groups which would match this project description"); 439 out.println("-i ~/c.xml -d -n Abcd # => deletes group Abcd"); 440 out.println("-n Bcde -r \".*bcde.*\" -l # => add group and lists the result"); 441 } 442 } 443