Help - Added a task to find unused help images; fixed showing in-memory

images in help
This commit is contained in:
dragonmacher 2019-04-24 17:48:44 -04:00
parent e8dcb3d0c4
commit 49436c44a9
12 changed files with 275 additions and 131 deletions

View File

@ -15,7 +15,6 @@
*/
package docking.help;
import java.awt.Image;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.*;
@ -25,7 +24,6 @@ import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.ImageIcon;
import javax.swing.JEditorPane;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
@ -380,20 +378,10 @@ public class GHelpHTMLEditorKit extends HTMLEditorKit {
*/
private class GHelpImageView extends ImageView {
private Image myImage;
public GHelpImageView(Element elem) {
super(elem);
}
@Override
public Image getImage() {
if (myImage != null) {
return myImage;
}
return super.getImage();
}
@Override
public URL getImageURL() {
@ -419,10 +407,7 @@ public class GHelpHTMLEditorKit extends HTMLEditorKit {
return null;
}
ImageIcon icon = iconProvider.getIcon();
myImage = icon.getImage();
URL url = iconProvider.getUrl();
URL url = iconProvider.getOrCreateUrl();
return url;
}

View File

@ -15,17 +15,30 @@
*/
package resources;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import javax.swing.ImageIcon;
import generic.Images;
import generic.util.image.ImageUtils;
import ghidra.util.Msg;
/**
* A class that knows how to provide an icon and the URL for that icon
* A class that knows how to provide an icon and the URL for that icon. If {@link #getUrl()}
* returns a non-null value, then that is the URL used to originally load the icon in this class.
*
* <p>If {@link #getUrl()} returns null, then {@link #getOrCreateUrl()} can be used to create a
* value URL by writing out the image for this class's icon.
*/
public class IconProvider {
private ImageIcon icon;
private URL url;
private URL tempUrl;
private boolean tempFileFailed;
public IconProvider(ImageIcon icon, URL url) {
this.icon = icon;
@ -36,11 +49,76 @@ public class IconProvider {
return icon;
}
public boolean isInvalid() {
return icon == null; // as long as we have an icon, we are valid, url or not
}
public URL getUrl() {
return url;
}
public boolean isInvalid() {
return icon == null || url == null;
/**
* Returns the value of {@link #getUrl()} if it is non-null. Otherwise, this class will
* attempt to create a temporary file containing the image of this class in order to return
* a URL for that temp file. If a temporary file could not be created, then the URL
* returned from this class will point to the
* {@link ResourceManager#getDefaultIcon() default icon}.
*
* @return the URL
*/
public URL getOrCreateUrl() {
if (url != null) {
return url;
}
createTempUrlAsNeeded();
return tempUrl;
}
private void createTempUrlAsNeeded() {
if (testUrl(tempUrl)) {
return;
}
tempUrl = createTempUrl();
if (tempUrl == null) {
tempUrl = getDefaultUrl();
}
}
private URL createTempUrl() {
if (tempFileFailed) {
return null; // don't repeatedly attempt to create a temp file
}
try {
File imageFile = File.createTempFile("temp.help.icon", null);
imageFile.deleteOnExit(); // don't let this linger
ImageUtils.writeFile(icon.getImage(), imageFile);
return imageFile.toURI().toURL();
}
catch (IOException e) {
tempFileFailed = true;
Msg.error(this, "Unable to write temp image to display in help for " +
ResourceManager.getIconName(icon));
}
return null;
}
private boolean testUrl(URL testUrl) {
if (testUrl == null) {
return false;
}
try {
return new File(testUrl.toURI()).exists();
}
catch (URISyntaxException e) {
return false;
}
}
private URL getDefaultUrl() {
return ResourceManager.getResource(Images.BOMB);
}
}

View File

@ -91,7 +91,7 @@ public class Icons {
ResourceManager.loadImage("images/dialog-cancel.png", 10, 10), 6, 6)));
public static final ImageIcon APPLY_BLOCKED_MATCH_ICON = ResourceManager.getImageIcon(
new MultiIcon(ResourceManager.loadImage("images/kgpg.png"), new TranslateIcon(
ResourceManager.loadImage("images/checkmark_green.png", 12, 12), 4, 0)));
ResourceManager.loadImage("images/checkmark_green.gif", 12, 12), 4, 0)));
/**
* Returns true if the given string is a Java code snippet that references this class
@ -126,24 +126,6 @@ public class Icons {
return new IconProvider(icon, url);
}
/**
* Returns a URL for the given code snippet if it is a field reference on this class
*
* @param snippet the snippet of Java code that references a field of this class
* @return the URL; null if the snippet does not refer to a field of this class
*/
public static URL getUrlForIconsReference(String snippet) {
String fieldName = getIconName(snippet);
if (fieldName == null) {
return null;
}
ImageIcon icon = getIconByFieldName(fieldName);
URL url = getUrlFromIcon(icon);
return url;
}
private static String getIconName(String snippet) {
if (!isIconsReference(snippet)) {
return null;
@ -185,7 +167,7 @@ public class Icons {
return url;
}
catch (MalformedURLException e) {
Msg.debug(Icons.class, "Unable to get URL for icon: " + description, e);
Msg.trace(Icons.class, "Unable to get URL for icon: " + description);
return null;
}

View File

@ -160,19 +160,18 @@ public class MultiIcon implements Icon {
@Override
public String toString() {
// return getClass().getSimpleName() + "[" + getIconNames() + "]";
return getDescription();
return getClass().getSimpleName() + "[" + getIconNames() + "]";
}
// private String getIconNames() {
// StringBuffer buffy = new StringBuffer();
// for (Icon icon : iconList) {
// if (buffy.length() > 0) {
// buffy.append(", ");
// }
// buffy.append(ResourceManager.getIconName(icon));
// }
//
// return buffy.toString();
// }
private String getIconNames() {
StringBuffer buffy = new StringBuffer();
for (Icon icon : iconList) {
if (buffy.length() > 0) {
buffy.append(", ");
}
buffy.append(ResourceManager.getIconName(icon));
}
return buffy.toString();
}
}

View File

@ -400,14 +400,18 @@ public class ResourceManager {
}
/**
* Get the name of this icon. If icon is an ImageIcon, its getDescription() is called to
* get the name
* Get the name of this icon. The value is usually going to be the URL from which the icon
* was loaded
*
* @param icon the icon for which the name is desired
* @return the name
* @return the name
*/
public static String getIconName(Icon icon) {
String iconName = icon.toString();
if (icon instanceof FileBasedIcon) {
return ((FileBasedIcon) icon).getFilename();
}
if (icon instanceof ImageIcon) {
iconName = ((ImageIcon) icon).getDescription();
}

View File

@ -25,6 +25,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import help.validator.location.*;
import resources.IconProvider;
import resources.Icons;
public class HelpBuildUtils {
@ -536,6 +537,7 @@ public class HelpBuildUtils {
* locate files based upon relative references, specialized help system references (i.e.,
* help/topics/...), and absolute URLs.
*
* @param sourceFile the source file path of the image reference
* @param ref the reference text
* @return an absolute path; null if the URI is remote
* @throws URISyntaxException
@ -544,15 +546,21 @@ public class HelpBuildUtils {
throws URISyntaxException {
if (Icons.isIconsReference(ref)) {
// help system syntax: <img src="Icons.ERROR_ICON" />
URL url = Icons.getUrlForIconsReference(ref);
if (url == null) {
IconProvider iconProvider = Icons.getIconForIconsReference(ref);
if (iconProvider == null || iconProvider.isInvalid()) {
// bad icon name
return ImageLocation.createInvalidRuntimeLocation(sourceFile, ref);
}
URI resolved = url.toURI();
Path path = toPath(resolved);
URL url = iconProvider.getUrl();
URI resolved = null;
Path path = null;
if (url != null) { // we may have an icon with an invalid URL (e.g., a MultiIcon)
resolved = url.toURI();
path = toPath(resolved);
}
return ImageLocation.createRuntimeLocation(sourceFile, ref, resolved, path);
}

View File

@ -21,6 +21,9 @@ import java.nio.file.Path;
/**
* A class that represents the original location of an IMG tag along with its location
* resolution within the help system.
*
* <p>Some images are represented by 'in memory' or 'runtime' values that do not have a valid
* url.
*/
public class ImageLocation {
@ -30,8 +33,13 @@ public class ImageLocation {
private Path resolvedPath;
private URI resolvedUri;
private boolean isRemote;
/** An image that is taken from an image loaded by a Java class (e.g., Icons.XYZ_ICON) */
private boolean isRuntime;
/** A 'runtime' image that could not be located */
private boolean invalidRuntimeImage;
public static ImageLocation createLocalLocation(Path sourceFile, String imageSrc,
URI resolvedUri, Path resolvedPath) {
@ -61,6 +69,7 @@ public class ImageLocation {
l.resolvedPath = null;
l.isRemote = false;
l.isRuntime = true;
l.invalidRuntimeImage = true;
return l;
}
@ -84,48 +93,28 @@ public class ImageLocation {
return sourceFile;
}
public void setSourceFile(Path sourceFile) {
this.sourceFile = sourceFile;
}
public String getImageSrc() {
return imageSrc;
}
public void setImageSrc(String imageSrc) {
this.imageSrc = imageSrc;
}
public Path getResolvedPath() {
return resolvedPath;
}
public void setResolvedPath(Path resolvedPath) {
this.resolvedPath = resolvedPath;
}
public URI getResolvedUri() {
return resolvedUri;
}
public void setResolvedUri(URI resolvedUri) {
this.resolvedUri = resolvedUri;
}
public boolean isRemote() {
return isRemote;
}
public void setRemote(boolean isRemote) {
this.isRemote = isRemote;
}
public boolean isRuntime() {
return isRuntime;
}
public void setRuntime(boolean isRuntime) {
this.isRuntime = isRuntime;
public boolean isInvalidRuntimeImage() {
return invalidRuntimeImage;
}
@Override

View File

@ -120,14 +120,22 @@ public class JavaHelpValidator {
return; // don't even try to verify a remote URL
}
Path imagePath = img.getImageFile();
if (imagePath == null) {
unresolvedLinks.add(new NonExistentIMGFileInvalidLink(img));
if (img.isRuntime()) {
//
// The tool will load this image at runtime--don't perform normal validation
// (runtime means an icon to be loaded from a Java file)
//
if (img.isInvalid()) {
unresolvedLinks.add(new InvalidRuntimeIMGFileInvalidLink(img));
return;
}
return;
}
if (img.isRuntime()) {
// the tool will load this image at runtime--don't perform normal validate
Path imagePath = img.getImageFile();
if (imagePath == null) {
unresolvedLinks.add(new NonExistentIMGFileInvalidLink(img));
return;
}

View File

@ -1,6 +1,5 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,22 +15,25 @@
*/
package help.validator;
import help.GHelpBuilder;
import help.HelpBuildUtils;
import help.validator.location.HelpModuleLocation;
import help.validator.model.IMG;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import org.apache.commons.lang3.StringUtils;
import help.HelpBuildUtils;
import help.validator.location.HelpModuleLocation;
import help.validator.model.IMG;
import util.CollectionUtils;
public class UnusedHelpImageFileFinder {
private static final String HELP_PATHS_OPTION = "-hp"; // taken from GHelpBuilder
private static final String DEBUG_SWITCH = "-debug";
private static List<String> moduleHelpPaths;
private static List<String> moduleHelpPaths = new ArrayList<>();
private static boolean debugEnabled = false;
private SortedSet<Path> unusedFiles;
@ -78,24 +80,20 @@ public class UnusedHelpImageFileFinder {
}
public SortedSet<Path> getUnusedImages() {
return new TreeSet<Path>(unusedFiles);
return new TreeSet<>(unusedFiles);
}
private static SortedSet<Path> getUnusedFiles(Collection<IMG> referencedIMGs,
Collection<Path> imageFiles) {
Map<Path, IMG> fileToIMGMap = new HashMap<Path, IMG>();
Map<Path, IMG> fileToIMGMap = new HashMap<>();
for (IMG img : referencedIMGs) {
fileToIMGMap.put(img.getImageFile(), img);
}
SortedSet<Path> set = new TreeSet<Path>(new Comparator<Path>() {
@Override
public int compare(Path f1, Path f2) {
return f1.toUri().toString().toLowerCase().compareTo(
f2.toUri().toString().toLowerCase());
}
});
SortedSet<Path> set =
new TreeSet<>((f1, f2) -> f1.toUri().toString().toLowerCase().compareTo(
f2.toUri().toString().toLowerCase()));
for (Path file : imageFiles) {
IMG img = fileToIMGMap.get(file);
if (img == null && !isExcludedImageFile(file)) {
@ -111,8 +109,9 @@ public class UnusedHelpImageFileFinder {
return absolutePath.indexOf("help/shared/") != -1;
}
private static Collection<IMG> getReferencedIMGs(Collection<HelpModuleLocation> helpCollections) {
Set<IMG> set = new HashSet<IMG>();
private static Collection<IMG> getReferencedIMGs(
Collection<HelpModuleLocation> helpCollections) {
Set<IMG> set = new HashSet<>();
for (HelpModuleLocation help : helpCollections) {
Collection<IMG> IMGs = help.getAllIMGs();
set.addAll(IMGs);
@ -122,7 +121,7 @@ public class UnusedHelpImageFileFinder {
private static Collection<Path> getAllImagesOnDisk(
Collection<HelpModuleLocation> helpDirectories) {
List<Path> files = new ArrayList<Path>();
List<Path> files = new ArrayList<>();
for (HelpModuleLocation help : helpDirectories) {
Path helpDir = help.getHelpLocation();
gatherImageFiles(helpDir, files);
@ -155,10 +154,10 @@ public class UnusedHelpImageFileFinder {
private static List<HelpModuleLocation> collectHelp() {
debug("Parsing help dirs...");
List<HelpModuleLocation> helpCollections =
new ArrayList<HelpModuleLocation>(moduleHelpPaths.size());
List<HelpModuleLocation> helpCollections = new ArrayList<>(moduleHelpPaths.size());
for (String helpDirName : moduleHelpPaths) {
// 1) Make sure the help directory exists
// Make sure the help directory exists
File helpDirectoryFile = null;
try {
helpDirectoryFile = new File(helpDirName).getCanonicalFile();
@ -172,14 +171,8 @@ public class UnusedHelpImageFileFinder {
errorMessage("Help directory not found - skipping: " + helpDirName);
continue;
}
File moduleDir = helpDirectoryFile.getParentFile();
File manifestFile = new File(moduleDir, "Module.manifest");
if (!manifestFile.exists()) {
errorMessage("Help directory not inside valid module: " + helpDirName);
continue;
}
// 3) Create the help directory
// Create the help directory
helpCollections.add(HelpBuildUtils.toLocation(helpDirectoryFile));
}
@ -188,8 +181,8 @@ public class UnusedHelpImageFileFinder {
private static void debug(String string) {
if (debugEnabled) {
System.out.println("[" + UnusedHelpImageFileFinder.class.getSimpleName() + "] " +
string);
System.out.println(
"[" + UnusedHelpImageFileFinder.class.getSimpleName() + "] " + string);
}
}
@ -197,7 +190,7 @@ public class UnusedHelpImageFileFinder {
StringBuilder buffy = new StringBuilder();
errorMessage("Usage:\n");
buffy.append("<module help path1[;module help path2;module help path3;...]> [-debug]");
buffy.append("-hp path1[-hp path2 -hp path3 ...]> [-debug]");
errorMessage(buffy.toString());
}
@ -209,23 +202,53 @@ public class UnusedHelpImageFileFinder {
System.exit(1);
}
List<String> argList = Arrays.asList(args);
// get module directory paths
String modulePathsString = argList.get(args.length - 1);
moduleHelpPaths = new ArrayList<String>();
StringTokenizer tokenizer = new StringTokenizer(modulePathsString, File.pathSeparator);
while (tokenizer.hasMoreTokens()) {
moduleHelpPaths.add(tokenizer.nextToken());
List<String> argList = CollectionUtils.asList(args);
int debugIndex = argList.indexOf(DEBUG_SWITCH);
if (debugIndex > -1) {
debugEnabled = true;
argList.remove(debugIndex);
}
Map<Integer, String> mapped = new TreeMap<>();
for (int i = 0; i < argList.size(); i++) {
mapped.put(i, argList.get(i));
}
for (int i = 0; i < argList.size(); i++) {
String opt = argList.get(i);
if (opt.equals(HELP_PATHS_OPTION)) {
if (i >= argList.size()) {
errorMessage(HELP_PATHS_OPTION + " requires an argument");
printUsage();
System.exit(1);
}
mapped.remove(i);
String paths = mapped.remove(++i);
if (StringUtils.isBlank(paths)) {
errorMessage(HELP_PATHS_OPTION + " requires an argument");
printUsage();
System.exit(1);
}
// each entry should be just one value, but handle multiple paths anyway
for (String p : paths.split(File.pathSeparator)) {
moduleHelpPaths.add(p);
}
}
}
if (moduleHelpPaths.size() == 0) {
errorMessage("Missing molule help path(s) argument - it must be last in the arg list");
errorMessage(
"Missing molule help path(s) arguments - actual arguments:\n\t'" + argList + "'");
printUsage();
System.exit(1);
}
int debugIndex = argList.indexOf(DEBUG_SWITCH);
debugEnabled = debugIndex != -1;
if (!mapped.isEmpty()) {
errorMessage("Ignoring unknown arguments: " + mapped.values());
}
}
private static void errorMessage(String message) {
@ -233,7 +256,7 @@ public class UnusedHelpImageFileFinder {
}
private static void errorMessage(String message, Throwable t) {
System.err.println("[" + GHelpBuilder.class.getSimpleName() + "] " + message);
System.err.println("[" + UnusedHelpImageFileFinder.class.getSimpleName() + "] " + message);
if (t != null) {
t.printStackTrace();
}

View File

@ -0,0 +1,32 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package help.validator.links;
import help.validator.model.IMG;
/**
* A link that represents the case where the HTML tried to reference a runtime Java image, but
* that value is not found
*/
public class InvalidRuntimeIMGFileInvalidLink extends InvalidIMGLink {
private static final String MESSAGE =
"Runtime image not found (e.g., Icons.XYZ_ICON not found)";
public InvalidRuntimeIMGFileInvalidLink(IMG img) {
super(img, MESSAGE);
}
}

View File

@ -79,6 +79,10 @@ public class IMG implements Comparable<IMG> {
return imageLocation.isRuntime();
}
public boolean isInvalid() {
return imageLocation.isInvalidRuntimeImage();
}
public Path getImageFile() {
return imgFile;
}

View File

@ -141,10 +141,42 @@ task buildHelp(type: JavaExec, dependsOn: indexHelp) {
}
// Task for finding unused images that are not referenced from Ghidra help files
task findUnusedHelp(type: JavaExec) {
group rootProject.GHIDRA_GROUP
description " Finds unused help images for this module. [gradle/helpProject.gradle]\n"
File helpRootDir = file('src/main/help/help')
File outputDir = file('build/help/main/help')
dependsOn configurations.helpPath
inputs.dir helpRootDir
classpath = sourceSets.helpIndex.runtimeClasspath
main = 'help.validator.UnusedHelpImageFileFinder'
args '-debug' // print debug info
doFirst {
// this modules runtime classpath (contains jhall.jar)
classpath project(':Help').sourceSets.main.runtimeClasspath
// the current help dir to process
args "-hp"
args "${helpRootDir.absolutePath}"
}
}
// include the help into the module's jar
jar {
from "build/help/main" // include the generated help index files
from "src/main/help" // include the help source files
from "src/main/help" // include the help source files
}
// build the help whenever this module's jar file is built