blob: bf74beec24cb4930e0b4d76e068fc1224c80e693 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2002, 2019 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.ui.internal.cheatsheets.registry;
import java.text.Collator;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtensionDelta;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.IRegistryChangeEvent;
import org.eclipse.core.runtime.IRegistryChangeListener;
import org.eclipse.core.runtime.Platform;
import org.eclipse.ui.internal.cheatsheets.CheatSheetPlugin;
import org.eclipse.ui.internal.cheatsheets.ICheatSheetResource;
import org.eclipse.ui.internal.cheatsheets.Messages;
/**
* Instances access the registry that is provided at creation time
* in order to determine the contained CheatSheet Contents
*/
public class CheatSheetRegistryReader extends RegistryReader implements IRegistryChangeListener {
private static class CategoryNode {
private Category category;
private String path;
public CategoryNode(Category cat) {
category = cat;
path = ICheatSheetResource.EMPTY_STRING;
String[] categoryPath = category.getParentPath();
if (categoryPath != null) {
for (int nX = 0; nX < categoryPath.length; nX++) {
path += categoryPath[nX] + '/';
}
}
path += cat.getId();
}
public Category getCategory() {
return category;
}
public String getPath() {
return path;
}
}
/**
* Represents a taskEditor entry in the registry
*/
public static class TaskEditorNode {
private String className;
private String iconPath;
private String id;
private String pluginId;
public void setClassName(String className) {
this.className = className;
}
public String getClassName() {
return className;
}
public void setIconPath(String iconPath) {
this.iconPath = iconPath;
}
public String getIconPath() {
return iconPath;
}
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
public void setPluginId(String pluginId) {
this.pluginId = pluginId;
}
public String getPluginId() {
return pluginId;
}
}
/**
* Represents a taskExplorer entry in the registry
*/
public static class TaskExplorerNode {
private String className;
private String iconPath;
private String name;
private String id;
private String pluginId;
public void setClassName(String className) {
this.className = className;
}
public String getClassName() {
return className;
}
public void setIconPath(String iconPath) {
this.iconPath = iconPath;
}
public String getIconPath() {
return iconPath;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
public void setPluginId(String pluginId) {
this.pluginId = pluginId;
}
public String getPluginId() {
return pluginId;
}
}
// constants
private final static String ATT_CATEGORY = "category"; //$NON-NLS-1$
public final static String ATT_CONTENTFILE = "contentFile"; //$NON-NLS-1$
protected final static String ATT_ICON = "icon"; //$NON-NLS-1$
protected final static String ATT_ID = "id"; //$NON-NLS-1$
protected final static String ATT_LISTENERCLASS = "listener"; //$NON-NLS-1$
protected final static String ATT_NAME = "name"; //$NON-NLS-1$
protected final static String ATT_CLASS = "class"; //$NON-NLS-1$
private final static String ATT_COMPOSITE = "composite"; //$NON-NLS-1$
private final static String CATEGORY_SEPARATOR = "/"; //$NON-NLS-1$
private final static String ATT_ITEM_ATTRIBUTE = "itemAttribute"; //$NON-NLS-1$
private static CheatSheetRegistryReader instance;
private final static String TAG_CATEGORY = "category"; //$NON-NLS-1$
public final static String TAG_CHEATSHEET = "cheatsheet"; //$NON-NLS-1$
protected final static String TAG_ITEM_EXTENSION = "itemExtension"; //$NON-NLS-1$
protected final static String TAG_TASK_EDITOR = "taskEditor"; //$NON-NLS-1$
protected final static String TAG_TASK_EXPLORER = "taskExplorer"; //$NON-NLS-1$
protected final static String trueString = "TRUE"; //$NON-NLS-1$
private final static String UNCATEGORIZED_CHEATSHEET_CATEGORY = "org.eclipse.ui.Other"; //$NON-NLS-1$
private final static String UNCATEGORIZED_CHEATSHEET_CATEGORY_LABEL = Messages.CHEAT_SHEET_OTHER_CATEGORY;
public final static String CHEAT_SHEET_CONTENT = "cheatSheetContent"; //$NON-NLS-1$
/**
* Returns a list of cheatsheets, project and not.
*
* The return value for this method is cached since computing its value
* requires non-trivial work.
*/
public static CheatSheetRegistryReader getInstance() {
if (instance == null) {
instance = new CheatSheetRegistryReader();
IExtensionRegistry xregistry = Platform.getExtensionRegistry();
xregistry.addRegistryChangeListener(instance, ICheatSheetResource.CHEAT_SHEET_PLUGIN_ID);
}
return instance;
}
protected ArrayList<CheatSheetItemExtensionElement> cheatsheetItemExtensions;
protected CheatSheetCollectionElement cheatsheets;
private ArrayList<Category> deferCategories = null;
private ArrayList<CheatSheetElement> deferCheatSheets = null;
private final String csItemExtension = "cheatSheetItemExtension"; //$NON-NLS-1$
protected Map<String, TaskExplorerNode> taskExplorers = new HashMap<>();
protected Map<String, TaskEditorNode> taskEditors = new HashMap<>();
private Map<String, Object> nestedCategoryIds = new HashMap<>();
/**
* Create an instance of this class.
*/
private CheatSheetRegistryReader() {
}
/**
* Adds new cheatsheet to the provided collection. Override to
* provide more logic.
* <p>
* This implementation uses a defering strategy. For more info see
* <code>readCheatSheets</code>.
* </p>
*/
protected void addNewElementToResult(CheatSheetElement element, IConfigurationElement config, CheatSheetCollectionElement cheatsheets2) {
deferCheatSheet(element);
}
/**
* Returns a new CheatSheetElement configured according to the parameters
* contained in the passed Registry.
*
* May answer null if there was not enough information in the Extension to create
* an adequate cheatsheet
*/
protected CheatSheetElement createCheatSheetElement(IConfigurationElement element) {
// CheatSheetElements must have a name attribute
String nameString = element.getAttribute(ATT_NAME);
if (nameString == null) {
logMissingAttribute(element, ATT_NAME);
return null;
}
CheatSheetElement result = new CheatSheetElement(nameString);
if (initializeCheatSheet(result, element))
return result; // ie.- initialization was successful
return null;
}
/**
* Create and answer a new CheatSheetCollectionElement, configured as a
* child of <code>parent</code>
*
* @return org.eclipse.ui.internal.model.CheatSheetCollectionElement
* @param parent org.eclipse.ui.internal.model.CheatSheetCollectionElement
* @param childName java.lang.String
*/
protected CheatSheetCollectionElement createCollectionElement(CheatSheetCollectionElement parent, String pluginId, String id, String label) {
CheatSheetCollectionElement newElement = new CheatSheetCollectionElement(pluginId, id, label, parent);
parent.add(newElement);
return newElement;
}
/**
* Creates empty element collection. Overrider to fill
* initial elements, if needed.
*/
protected CheatSheetCollectionElement createEmptyCheatSheetCollection() {
return new CheatSheetCollectionElement(null, "root", "root", null); //$NON-NLS-1$//$NON-NLS-2$
}
/**
* Stores a category element for deferred addition.
*/
private void deferCategory(IConfigurationElement config) {
// Create category.
Category category = null;
try {
category = new Category(config);
} catch (CoreException e) {
CheatSheetPlugin.getPlugin().getLog().log(e.getStatus());
return;
}
// Defer for later processing.
if (deferCategories == null)
deferCategories = new ArrayList<>(20);
deferCategories.add(category);
}
/**
* Stores a cheatsheet element for deferred addition.
*/
private void deferCheatSheet(CheatSheetElement element) {
if (deferCheatSheets == null)
deferCheatSheets = new ArrayList<>(50);
deferCheatSheets.add(element);
}
/**
* Returns the first cheatsheet
* with a given id.
*/
public CheatSheetElement findCheatSheet(String id) {
Object[] cheatsheetsList = getCheatSheets().getChildren();
for (Object element2 : cheatsheetsList) {
CheatSheetCollectionElement collection = (CheatSheetCollectionElement) element2;
CheatSheetElement element = collection.findCheatSheet(id, true);
if (element != null)
return element;
}
return null;
}
/**
* Returns the first task editor
* with a given id.
*/
public TaskEditorNode findTaskEditor(String id) {
if (cheatsheets == null) {
readCheatSheets(); // Ensure that the registry has been read
}
return taskEditors.get(id);
}
/**
* Returns the first task explorer
* with a given id.
*/
public TaskExplorerNode findTaskExplorer(String id) {
if (cheatsheets == null) {
readCheatSheets(); // Ensure that the registry has been read
}
return taskExplorers.get(id);
}
/**
* Get the list of explorer ids
* @return an iterator for the explorer ids
*/
public String[] getExplorerIds() {
if (cheatsheets == null) {
readCheatSheets(); // Ensure that the registry has been read
}
Set<String> keys = taskExplorers.keySet();
return keys.toArray(new String[keys.size()]);
}
/**
* Finishes the addition of categories. The categories are sorted and
* added in a root to depth traversal.
*/
private void finishCategories() {
// If no categories just return.
if (deferCategories == null)
return;
// Sort categories by flattened name.
CategoryNode[] flatArray = new CategoryNode[deferCategories.size()];
for (int i = 0; i < deferCategories.size(); i++) {
flatArray[i] = new CategoryNode(deferCategories.get(i));
}
Sorter sorter = new Sorter() {
private Collator collator = Collator.getInstance();
@Override
public boolean compare(Object o1, Object o2) {
String s1 = ((CategoryNode) o1).getPath();
String s2 = ((CategoryNode) o2).getPath();
return collator.compare(s2, s1) > 0;
}
};
Object[] sortedCategories = sorter.sort(flatArray);
// Add each category.
for (int nX = 0; nX < sortedCategories.length; nX++) {
Category cat = ((CategoryNode) sortedCategories[nX]).getCategory();
finishCategory(cat);
}
// Cleanup.
deferCategories = null;
}
/**
* Save new category definition.
*/
private void finishCategory(Category category) {
CheatSheetCollectionElement currentResult = cheatsheets;
String[] categoryPath = category.getParentPath();
CheatSheetCollectionElement parent = currentResult; // ie.- root
// Traverse down into parent category.
if (categoryPath != null) {
nestedCategoryIds.put(category.getId(), category);
for (String element : categoryPath) {
CheatSheetCollectionElement tempElement = getChildWithID(parent, element);
if (tempElement == null) {
// The parent category is invalid. By returning here the
// category will be dropped and any cheatsheet within the category
// will be added to the "Other" category.
return;
}
parent = tempElement;
}
}
// If another category already exists with the same id ignore this one.
Object test = getChildWithID(parent, category.getId());
if (test != null)
return;
if (parent != null) {
CheatSheetCollectionElement collectionElement =
createCollectionElement(parent, category.getPluginId(), category.getId(), category.getLabel());
if (categoryPath != null) {
nestedCategoryIds.put(category.getId(), collectionElement);
}
}
}
/**
* Insert the passed cheatsheet element into the cheatsheet collection appropriately
* based upon its defining extension's CATEGORY tag value
*
* @param element CheatSheetElement
* @param extension
* @param currentResult CheatSheetCollectionElement
*/
private void finishCheatSheet(CheatSheetElement element, IConfigurationElement config, CheatSheetCollectionElement result) {
CheatSheetCollectionElement currentResult = result;
String category = getCategoryStringFor(config);
StringTokenizer familyTokenizer = new StringTokenizer(category, CATEGORY_SEPARATOR);
// use the period-separated sections of the current CheatSheet's category
// to traverse through the NamedSolution "tree" that was previously created
CheatSheetCollectionElement currentCollectionElement = currentResult; // ie.- root
boolean moveToOther = false;
while (familyTokenizer.hasMoreElements()) {
CheatSheetCollectionElement tempCollectionElement = getChildWithID(currentCollectionElement, familyTokenizer.nextToken());
if (tempCollectionElement == null) { // can't find the path; look for a simple path
moveToOther = true;
break;
}
currentCollectionElement = tempCollectionElement;
}
if (moveToOther) {
if (nestedCategoryIds.containsKey(category)) {
currentCollectionElement = (CheatSheetCollectionElement) nestedCategoryIds.get(category);
currentCollectionElement.add(element);
} else {
moveElementToUncategorizedCategory(currentResult, element);
}
} else {
currentCollectionElement.add(element);
}
}
/**
* Finishes the addition of cheatsheets. The cheatsheets are processed and categorized.
*/
private void finishCheatSheets() {
if (deferCheatSheets != null) {
Iterator<CheatSheetElement> iter = deferCheatSheets.iterator();
while (iter.hasNext()) {
CheatSheetElement cheatsheet = iter.next();
IConfigurationElement config = cheatsheet.getConfigurationElement();
finishCheatSheet(cheatsheet, config, cheatsheets);
}
deferCheatSheets = null;
}
nestedCategoryIds = null;
}
/**
* Return the appropriate category (tree location) for this CheatSheet.
* If a category is not specified then return a default one.
*/
protected String getCategoryStringFor(IConfigurationElement config) {
String result = config.getAttribute(ATT_CATEGORY);
if (result == null)
result = UNCATEGORIZED_CHEATSHEET_CATEGORY;
return result;
}
/**
* Returns a list of cheatsheets, project and not.
*
* The return value for this method is cached since computing its value
* requires non-trivial work.
*/
public CheatSheetCollectionElement getCheatSheets() {
if (cheatsheets == null)
readCheatSheets();
return cheatsheets;
}
/**
* Go through the children of the passed parent and answer the child
* with the passed name. If no such child is found then return null.
*
* @return org.eclipse.ui.internal.model.CheatSheetCollectionElement
* @param parent org.eclipse.ui.internal.model.CheatSheetCollectionElement
* @param childName java.lang.String
*/
protected CheatSheetCollectionElement getChildWithID(CheatSheetCollectionElement parent, String id) {
Object[] children = parent.getChildren();
for (int i = 0; i < children.length; ++i) {
CheatSheetCollectionElement currentChild = (CheatSheetCollectionElement) children[i];
if (currentChild.getId().equals(id))
return currentChild;
}
return null;
}
/**
* Initialize the passed element's properties based on the contents of
* the passed registry. Answer a boolean indicating whether the element
* was able to be adequately initialized.
*
* @return boolean
* @param element CheatSheetElement
* @param extension Extension
*/
protected boolean initializeCheatSheet(CheatSheetElement element, IConfigurationElement config) {
element.setID(config.getAttribute(ATT_ID));
element.setDescription(getDescription(config));
element.setConfigurationElement(config);
element.setRegistered(true);
String contentFile = config.getAttribute(ATT_CONTENTFILE);
if (contentFile != null) {
element.setContentFile(contentFile);
}
// ensure that a contentfile was specified
if (element.getConfigurationElement() == null || element.getContentFile() == null) {
logMissingAttribute(config, ATT_CONTENTFILE);
return false;
}
String listenerClass = config.getAttribute(ATT_LISTENERCLASS);
if (listenerClass != null) {
element.setListenerClass(listenerClass);
}
String composite = config.getAttribute(ATT_COMPOSITE);
if (composite != null) {
element.setComposite(composite.equalsIgnoreCase(trueString));
}
return true;
}
/**
* Moves given element to "Other" category, previously creating one if missing.
*/
protected void moveElementToUncategorizedCategory(CheatSheetCollectionElement root, CheatSheetElement element) {
CheatSheetCollectionElement otherCategory = getChildWithID(root, UNCATEGORIZED_CHEATSHEET_CATEGORY);
if (otherCategory == null)
otherCategory = createCollectionElement(root, null, UNCATEGORIZED_CHEATSHEET_CATEGORY, UNCATEGORIZED_CHEATSHEET_CATEGORY_LABEL);
otherCategory.add(element);
}
/**
* Removes the empty categories from a cheatsheet collection.
*/
private void pruneEmptyCategories(CheatSheetCollectionElement parent) {
Object[] children = parent.getChildren();
for (Object element : children) {
CheatSheetCollectionElement child = (CheatSheetCollectionElement) element;
pruneEmptyCategories(child);
}
}
/**
* Reads the cheatsheets in a registry.
* <p>
* This implementation uses a defering strategy. All of the elements
* (categories, cheatsheets) are read. The categories are created as the read occurs.
* The cheatsheets are just stored for later addition after the read completes.
* This ensures that cheatsheet categorization is performed after all categories
* have been read.
* </p>
*/
protected void readCheatSheets() {
IExtensionRegistry xregistry = Platform.getExtensionRegistry();
if (cheatsheets == null) {
cheatsheets = createEmptyCheatSheetCollection();
readRegistry(xregistry, ICheatSheetResource.CHEAT_SHEET_PLUGIN_ID, CHEAT_SHEET_CONTENT);
}
finishCategories();
finishCheatSheets();
if (cheatsheets != null) {
CheatSheetCollectionElement parent = cheatsheets;
pruneEmptyCategories(parent);
}
}
public ArrayList<CheatSheetItemExtensionElement> readItemExtensions() {
if (cheatsheetItemExtensions == null) {
cheatsheetItemExtensions = new ArrayList<>();
IExtensionRegistry xregistry = Platform.getExtensionRegistry();
//Now read the cheat sheet extensions.
readRegistry(xregistry, ICheatSheetResource.CHEAT_SHEET_PLUGIN_ID, csItemExtension);
}
return cheatsheetItemExtensions;
}
private void createItemExtensionElement(IConfigurationElement element) {
String className = element.getAttribute(ATT_CLASS);
String itemAttribute = element.getAttribute(ATT_ITEM_ATTRIBUTE);
// ensure that a class was specified
if (className == null) {
logMissingAttribute(element, ATT_CLASS);
return;
}
// ensure that a itemAttribute was specified
if (itemAttribute == null) {
logMissingAttribute(element, ATT_ITEM_ATTRIBUTE);
return;
}
CheatSheetItemExtensionElement itemExtensionElement = new CheatSheetItemExtensionElement();
itemExtensionElement.setClassName(className);
itemExtensionElement.setItemAttribute(itemAttribute);
itemExtensionElement.setConfigurationElement(element);
cheatsheetItemExtensions.add(itemExtensionElement);
}
/*
* Get a required attribute. Log an error if it has no value.
*/
private String getAndCheckAttribute(IConfigurationElement element, String name) {
String result = element.getAttribute(name);
if (result == null) {
logMissingAttribute(element, name);
}
return result;
}
private void createTaskExplorerElement(IConfigurationElement element) {
String icon = element.getAttribute(ATT_ICON);
String className = getAndCheckAttribute(element, ATT_CLASS);
String name = getAndCheckAttribute(element, ATT_NAME);
String id = getAndCheckAttribute(element, ATT_ID);
String pluginId = element.getContributor().getName();
if (id != null && className != null && name != null ) {
TaskExplorerNode node = new TaskExplorerNode();
node.setId(id);
node.setIconPath(icon);
node.setClassName(className);
node.setName(name);
node.setPluginId(pluginId);
taskExplorers.put(id, node);
}
}
private void createTaskEditorElement(IConfigurationElement element) {
String icon = getAndCheckAttribute(element, ATT_ICON);
String className = getAndCheckAttribute(element, ATT_CLASS);
String id = getAndCheckAttribute(element, ATT_ID);
String pluginId = element.getContributor().getName();
if (id != null && className != null && icon != null ) {
TaskEditorNode node = new TaskEditorNode();
node.setId(id);
node.setIconPath(icon);
node.setClassName(className);
node.setPluginId(pluginId);
taskEditors.put(id, node);
}
}
/**
* Implement this method to read element attributes.
*/
@Override
protected boolean readElement(IConfigurationElement element) {
if (element.getName().equals(TAG_CATEGORY)) {
deferCategory(element);
return true;
} else if (element.getName().equals(TAG_ITEM_EXTENSION)) {
createItemExtensionElement(element);
return true;
} else if (element.getName().equals(TAG_TASK_EDITOR)) {
createTaskEditorElement(element);
return true;
} else if (element.getName().equals(TAG_TASK_EXPLORER)) {
createTaskExplorerElement(element);
return true;
} else {
if (!element.getName().equals(TAG_CHEATSHEET))
return false;
CheatSheetElement cheatsheet = createCheatSheetElement(element);
if (cheatsheet != null)
addNewElementToResult(cheatsheet, element, cheatsheets);
return true;
}
}
@Override
public void registryChanged(IRegistryChangeEvent event) {
IExtensionDelta[] cheatSheetDeltas = event.getExtensionDeltas(ICheatSheetResource.CHEAT_SHEET_PLUGIN_ID, CHEAT_SHEET_CONTENT);
if (cheatSheetDeltas.length > 0) {
// reset the list of cheat sheets, it will be build on demand
cheatsheets = null;
}
IExtensionDelta[] itemExtensionDeltas = event.getExtensionDeltas(ICheatSheetResource.CHEAT_SHEET_PLUGIN_ID, csItemExtension);
if (itemExtensionDeltas.length > 0) {
// reset the list of cheat sheets item extensions, it will be build on demand
cheatsheetItemExtensions = null;
}
}
public void stop() {
IExtensionRegistry xregistry = Platform.getExtensionRegistry();
xregistry.removeRegistryChangeListener(instance);
instance = null;
}
}