xref: /OpenGrok/opengrok-indexer/src/main/java/org/opengrok/indexer/configuration/Groups.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) 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