1/*
2 * Copyright (c) 1998, 2017, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package jdk.javadoc.internal.doclets.toolkit.util;
27
28import java.util.*;
29
30import javax.lang.model.element.ModuleElement;
31import javax.lang.model.element.PackageElement;
32
33import jdk.javadoc.internal.doclets.toolkit.BaseConfiguration;
34import jdk.javadoc.internal.doclets.toolkit.Messages;
35
36
37/**
38 * Process and manage grouping of elements, as specified by "-group" option on
39 * the command line.
40 * <p>
41 * For example, if user has used -group option as
42 * -group "Core Packages" "java.*" -group "CORBA Packages" "org.omg.*", then
43 * the packages specified on the command line will be grouped according to their
44 * names starting with either "java." or "org.omg.". All the other packages
45 * which do not fall in the user given groups, are grouped in default group,
46 * named as either "Other Packages" or "Packages" depending upon if "-group"
47 * option used or not at all used respectively.
48 * </p>
49 * <p>
50 * Also the packages are grouped according to the longest possible match of
51 * their names with the grouping information provided. For example, if there
52 * are two groups, like -group "Lang" "java.lang" and -group "Core" "java.*",
53 * will put the package java.lang in the group "Lang" and not in group "Core".
54 * </p>
55 *
56 *  <p><b>This is NOT part of any supported API.
57 *  If you write code that depends on this, you do so at your own risk.
58 *  This code and its internal interfaces are subject to change or
59 *  deletion without notice.</b>
60 *
61 * @author Atul M Dambalkar
62 */
63public class Group {
64
65    /**
66     * Map of regular expressions with the corresponding group name.
67     */
68    private Map<String,String> regExpGroupMap = new HashMap<>();
69
70    /**
71     * List of regular expressions sorted according to the length. Regular
72     * expression with longest length will be first in the sorted order.
73     */
74    private List<String> sortedRegExpList = new ArrayList<>();
75
76    /**
77     * List of group names in the same order as given on the command line.
78     */
79    private List<String> groupList = new ArrayList<>();
80
81    /**
82     * Map of non-regular expressions(possible package or module names) with the
83     * corresponding group name.
84     */
85    private Map<String,String> elementNameGroupMap = new HashMap<>();
86
87    /**
88     * The global configuration information for this run.
89     */
90    private final BaseConfiguration configuration;
91    private Messages messages;
92
93    /**
94     * Since we need to sort the keys in the reverse order(longest key first),
95     * the compare method in the implementing class is doing the reverse
96     * comparison.
97     */
98    private static class MapKeyComparator implements Comparator<String> {
99        public int compare(String key1, String key2) {
100            return key2.length() - key1.length();
101        }
102    }
103
104    public Group(BaseConfiguration configuration) {
105        this.configuration = configuration;
106        messages = configuration.getMessages();
107    }
108
109    /**
110     * Depending upon the format of the module name provided in the "-group"
111     * option, generate two separate maps. There will be a map for mapping
112     * regular expression(only meta character allowed is '*' and that is at the
113     * end of the regular expression) on to the group name. And another map
114     * for mapping (possible) module names(if the name format doesn't contain
115     * meta character '*', then it is assumed to be a module name) on to the
116     * group name. This will also sort all the regular expressions found in the
117     * reverse order of their lengths, i.e. longest regular expression will be
118     * first in the sorted list.
119     *
120     * @param groupname       The name of the group from -group option.
121     * @param moduleNameFormList List of the module name formats.
122     */
123    public boolean checkModuleGroups(String groupname, String moduleNameFormList) {
124        String[] mdlPatterns = moduleNameFormList.split(":");
125        if (groupList.contains(groupname)) {
126            initMessages();
127            messages.warning("doclet.Groupname_already_used", groupname);
128            return false;
129        }
130        groupList.add(groupname);
131        for (String mdlPattern : mdlPatterns) {
132            if (mdlPattern.length() == 0) {
133                initMessages();
134                messages.warning("doclet.Error_in_grouplist", groupname, moduleNameFormList);
135                return false;
136            }
137            if (mdlPattern.endsWith("*")) {
138                mdlPattern = mdlPattern.substring(0, mdlPattern.length() - 1);
139                if (foundGroupFormat(regExpGroupMap, mdlPattern)) {
140                    return false;
141                }
142                regExpGroupMap.put(mdlPattern, groupname);
143                sortedRegExpList.add(mdlPattern);
144            } else {
145                if (foundGroupFormat(elementNameGroupMap, mdlPattern)) {
146                    return false;
147                }
148                elementNameGroupMap.put(mdlPattern, groupname);
149            }
150        }
151        Collections.sort(sortedRegExpList, new MapKeyComparator());
152        return true;
153    }
154
155    /**
156     * Depending upon the format of the package name provided in the "-group"
157     * option, generate two separate maps. There will be a map for mapping
158     * regular expression(only meta character allowed is '*' and that is at the
159     * end of the regular expression) on to the group name. And another map
160     * for mapping (possible) package names(if the name format doesn't contain
161     * meta character '*', then it is assumed to be a package name) on to the
162     * group name. This will also sort all the regular expressions found in the
163     * reverse order of their lengths, i.e. longest regular expression will be
164     * first in the sorted list.
165     *
166     * @param groupname       The name of the group from -group option.
167     * @param pkgNameFormList List of the package name formats.
168     */
169    public boolean checkPackageGroups(String groupname, String pkgNameFormList) {
170        String[] pkgPatterns = pkgNameFormList.split(":");
171        if (groupList.contains(groupname)) {
172            initMessages();
173            messages.warning("doclet.Groupname_already_used", groupname);
174            return false;
175        }
176        groupList.add(groupname);
177        for (String pkgPattern : pkgPatterns) {
178            if (pkgPattern.length() == 0) {
179                initMessages();
180                messages.warning("doclet.Error_in_grouplist", groupname, pkgNameFormList);
181                return false;
182            }
183            if (pkgPattern.endsWith("*")) {
184                pkgPattern = pkgPattern.substring(0, pkgPattern.length() - 1);
185                if (foundGroupFormat(regExpGroupMap, pkgPattern)) {
186                    return false;
187                }
188                regExpGroupMap.put(pkgPattern, groupname);
189                sortedRegExpList.add(pkgPattern);
190            } else {
191                if (foundGroupFormat(elementNameGroupMap, pkgPattern)) {
192                    return false;
193                }
194                elementNameGroupMap.put(pkgPattern, groupname);
195            }
196        }
197        Collections.sort(sortedRegExpList, new MapKeyComparator());
198        return true;
199    }
200
201    // Lazy init of the messages for now, because Group is created
202    // in BaseConfiguration before configuration is fully initialized.
203    private void initMessages() {
204        if (messages == null) {
205            messages = configuration.getMessages();
206        }
207    }
208
209    /**
210     * Search if the given map has the given element format.
211     *
212     * @param map Map to be searched.
213     * @param elementFormat The format to search.
214     *
215     * @return true if element name format found in the map, else false.
216     */
217    boolean foundGroupFormat(Map<String,?> map, String elementFormat) {
218        if (map.containsKey(elementFormat)) {
219            initMessages();
220            messages.error("doclet.Same_element_name_used", elementFormat);
221            return true;
222        }
223        return false;
224    }
225
226    /**
227     * Group the modules according the grouping information provided on the
228     * command line. Given a list of modules, search each module name in
229     * regular expression map as well as module name map to get the
230     * corresponding group name. Create another map with mapping of group name
231     * to the module list, which will fall under the specified group. If any
232     * module doesn't belong to any specified group on the command line, then
233     * a new group named "Other Modules" will be created for it. If there are
234     * no groups found, in other words if "-group" option is not at all used,
235     * then all the modules will be grouped under group "Modules".
236     *
237     * @param modules Specified modules.
238     * @return map of group names and set of module elements.
239     */
240    public Map<String, SortedSet<ModuleElement>> groupModules(Set<ModuleElement> modules) {
241        Map<String, SortedSet<ModuleElement>> groupModuleMap = new HashMap<>();
242        String defaultGroupName =
243            (elementNameGroupMap.isEmpty() && regExpGroupMap.isEmpty())?
244                configuration.getResources().getText("doclet.Modules") :
245                configuration.getResources().getText("doclet.Other_Modules");
246        // if the user has not used the default group name, add it
247        if (!groupList.contains(defaultGroupName)) {
248            groupList.add(defaultGroupName);
249        }
250        for (ModuleElement mdl : modules) {
251            String moduleName = mdl.isUnnamed() ? null : mdl.getQualifiedName().toString();
252            String groupName = mdl.isUnnamed() ? null : elementNameGroupMap.get(moduleName);
253            // if this module is not explicitly assigned to a group,
254            // try matching it to group specified by regular expression
255            if (groupName == null) {
256                groupName = regExpGroupName(moduleName);
257            }
258            // if it is in neither group map, put it in the default
259            // group
260            if (groupName == null) {
261                groupName = defaultGroupName;
262            }
263            getModuleList(groupModuleMap, groupName).add(mdl);
264        }
265        return groupModuleMap;
266    }
267
268    /**
269     * Group the packages according the grouping information provided on the
270     * command line. Given a list of packages, search each package name in
271     * regular expression map as well as package name map to get the
272     * corresponding group name. Create another map with mapping of group name
273     * to the package list, which will fall under the specified group. If any
274     * package doesn't belong to any specified group on the command line, then
275     * a new group named "Other Packages" will be created for it. If there are
276     * no groups found, in other words if "-group" option is not at all used,
277     * then all the packages will be grouped under group "Packages".
278     *
279     * @param packages Packages specified on the command line.
280     * @return map of group names and set of package elements
281     */
282    public Map<String, SortedSet<PackageElement>> groupPackages(Set<PackageElement> packages) {
283        Map<String, SortedSet<PackageElement>> groupPackageMap = new HashMap<>();
284        String defaultGroupName =
285            (elementNameGroupMap.isEmpty() && regExpGroupMap.isEmpty())?
286                configuration.getResources().getText("doclet.Packages") :
287                configuration.getResources().getText("doclet.Other_Packages");
288        // if the user has not used the default group name, add it
289        if (!groupList.contains(defaultGroupName)) {
290            groupList.add(defaultGroupName);
291        }
292        for (PackageElement pkg : packages) {
293            String pkgName = pkg.isUnnamed() ? null : configuration.utils.getPackageName(pkg);
294            String groupName = pkg.isUnnamed() ? null : elementNameGroupMap.get(pkgName);
295            // if this package is not explicitly assigned to a group,
296            // try matching it to group specified by regular expression
297            if (groupName == null) {
298                groupName = regExpGroupName(pkgName);
299            }
300            // if it is in neither group map, put it in the default
301            // group
302            if (groupName == null) {
303                groupName = defaultGroupName;
304            }
305            getPkgList(groupPackageMap, groupName).add(pkg);
306        }
307        return groupPackageMap;
308    }
309
310    /**
311     * Search for element name in the sorted regular expression
312     * list, if found return the group name.  If not, return null.
313     *
314     * @param elementName Name of element to be found in the regular
315     * expression list.
316     */
317    String regExpGroupName(String elementName) {
318        for (String regexp : sortedRegExpList) {
319            if (elementName.startsWith(regexp)) {
320                return regExpGroupMap.get(regexp);
321            }
322        }
323        return null;
324    }
325
326    /**
327     * For the given group name, return the package list, on which it is mapped.
328     * Create a new list, if not found.
329     *
330     * @param map Map to be searched for group name.
331     * @param groupname Group name to search.
332     */
333    SortedSet<PackageElement> getPkgList(Map<String, SortedSet<PackageElement>> map,
334            String groupname) {
335        return map.computeIfAbsent(groupname, g -> new TreeSet<>(configuration.utils.makePackageComparator()));
336    }
337
338    /**
339     * For the given group name, return the module list, on which it is mapped.
340     * Create a new list, if not found.
341     *
342     * @param map Map to be searched for group name.
343     * @param groupname Group name to search.
344     */
345    SortedSet<ModuleElement> getModuleList(Map<String, SortedSet<ModuleElement>> map,
346            String groupname) {
347        return map.computeIfAbsent(groupname, g -> new TreeSet<>(configuration.utils.makeModuleComparator()));
348    }
349
350    /**
351     * Return the list of groups, in the same order as specified
352     * on the command line.
353     */
354    public List<String> getGroupList() {
355        return groupList;
356    }
357}
358