// Near Infinity - An Infinity Engine Browser and Editor // Copyright (C) 2001 - 2005 Jon Olav Hauglid // See LICENSE.txt for license information package org.infinity.search; import java.awt.Component; import java.awt.Container; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.concurrent.ThreadPoolExecutor; import java.util.regex.Pattern; import javax.swing.BorderFactory; import javax.swing.ButtonGroup; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JTextField; import javax.swing.ProgressMonitor; import org.infinity.datatype.DecNumber; import org.infinity.gui.Center; import org.infinity.gui.ChildFrame; import org.infinity.icon.Icons; import org.infinity.resource.AbstractStruct; import org.infinity.resource.Resource; import org.infinity.resource.ResourceFactory; import org.infinity.resource.StructEntry; import org.infinity.resource.dlg.AbstractCode; import org.infinity.resource.key.ResourceEntry; import org.infinity.util.Debugging; import org.infinity.util.Misc; public final class AttributeSearcher implements Runnable, ActionListener { private static final String FMT_PROGRESS = "Processing resource %d/%d"; private final ChildFrame inputFrame; private final Component parent; private final JButton bsearch = new JButton("Search", Icons.getIcon(Icons.ICON_FIND_AGAIN_16)); private final JCheckBox cbwhole = new JCheckBox("Match whole word only"); private final JCheckBox cbcase = new JCheckBox("Match case"); private final JCheckBox cbnot = new JCheckBox("Negate result"); private final JRadioButton rbexact = new JRadioButton("Exact match"); private final JRadioButton rbless = new JRadioButton("Less than"); private final JRadioButton rbgreater = new JRadioButton("Greater than"); private final JTextField tfinput = new JTextField("", 15); private final List<ResourceEntry> files; private final StructEntry structEntry; private ReferenceHitFrame resultFrame; private Pattern regPattern; private int searchNumber; private int progressIndex; private ProgressMonitor progress; public AttributeSearcher(AbstractStruct struct, StructEntry structEntry, Component parent) { this.structEntry = structEntry; this.parent = parent; while (struct.getSuperStruct() != null) struct = struct.getSuperStruct(); String filename = struct.getResourceEntry().toString(); files = ResourceFactory.getResources(filename.substring(filename.lastIndexOf(".") + 1).toUpperCase(Locale.ENGLISH)); inputFrame = new ChildFrame("Find: " + structEntry.getName(), true); inputFrame.setIconImage(Icons.getIcon(Icons.ICON_FIND_16).getImage()); inputFrame.getRootPane().setDefaultButton(bsearch); tfinput.setText(structEntry.toString()); bsearch.addActionListener(this); tfinput.addActionListener(this); rbexact.addActionListener(this); rbless.addActionListener(this); rbgreater.addActionListener(this); ButtonGroup gb = new ButtonGroup(); gb.add(rbexact); gb.add(rbless); gb.add(rbgreater); rbexact.setMnemonic('e'); rbless.setMnemonic('l'); rbgreater.setMnemonic('g'); rbexact.setSelected(true); rbexact.setEnabled(structEntry instanceof DecNumber); rbless.setEnabled(structEntry instanceof DecNumber); rbgreater.setEnabled(structEntry instanceof DecNumber); cbcase.setEnabled(!rbexact.isEnabled()); Container pane = inputFrame.getContentPane(); GridBagLayout gbl = new GridBagLayout(); GridBagConstraints gbc = new GridBagConstraints(); pane.setLayout(gbl); JLabel label = new JLabel("Find what:"); label.setLabelFor(tfinput); label.setDisplayedMnemonic('f'); JPanel dirpanel = new JPanel(); dirpanel.add(new JPanel()); dirpanel.add(rbexact); dirpanel.add(rbless); dirpanel.add(rbgreater); dirpanel.setBorder(BorderFactory.createTitledBorder("Only valid for numbers")); JPanel matchpanel = new JPanel(); matchpanel.setLayout(new GridLayout(3, 1)); matchpanel.add(cbnot); matchpanel.add(cbwhole); matchpanel.add(cbcase); cbnot.setMnemonic('n'); cbwhole.setMnemonic('w'); cbcase.setMnemonic('m'); gbc.insets = new Insets(6, 6, 3, 3); gbc.weightx = 0.0; gbc.weighty = 0.0; gbc.fill = GridBagConstraints.NONE; gbl.setConstraints(label, gbc); pane.add(label); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 1.0; gbc.gridwidth = 2; gbc.insets.left = 3; gbl.setConstraints(tfinput, gbc); pane.add(tfinput); gbc.weightx = 0.0; gbc.insets.right = 6; gbc.gridwidth = GridBagConstraints.REMAINDER; gbl.setConstraints(bsearch, gbc); pane.add(bsearch); gbc.gridwidth = 2; gbc.insets = new Insets(3, 6, 6, 3); gbl.setConstraints(matchpanel, gbc); pane.add(matchpanel); gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.insets.left = 3; gbc.insets.right = 6; gbl.setConstraints(dirpanel, gbc); pane.add(dirpanel); inputFrame.pack(); Center.center(inputFrame, parent.getBounds()); inputFrame.setVisible(true); } // --------------------- Begin Interface ActionListener --------------------- @Override public void actionPerformed(ActionEvent event) { if (event.getSource() == bsearch || event.getSource() == tfinput) { inputFrame.setVisible(false); new Thread(this).start(); } else if (event.getSource() == rbexact) cbwhole.setEnabled(true); else if (event.getSource() == rbless || event.getSource() == rbgreater) cbwhole.setEnabled(false); } // --------------------- End Interface ActionListener --------------------- // --------------------- Begin Interface Runnable --------------------- @Override public void run() { String title = structEntry.getName() + " - " + tfinput.getText(); if (cbnot.isSelected()) { title = structEntry.getName() + " - not " + tfinput.getText(); } searchNumber = 0; String term = tfinput.getText(); if (structEntry instanceof DecNumber) { // decimal and hexadecimal notation is supported String s = term.toLowerCase(Locale.ENGLISH); int radix = 0; if (s.length() > 1 && s.charAt(s.length() - 1) == 'h') { s = s.substring(0, s.length() - 1).trim(); radix = 16; } else if (s.matches("^-?0x[0-9a-f]+$")) { if (s.charAt(0) == '-') { s = '-' + s.substring(3).trim(); } else { s = s.substring(2).trim(); } radix = 16; } else if (s.matches("^-?[0-9a-f]+$") && !s.matches("^-?[0-9]+$")) { radix = 16; } else { s = s.trim(); radix = 10; } try { searchNumber = Integer.parseInt(s, radix); } catch (NumberFormatException e) { inputFrame.setVisible(true); JOptionPane.showMessageDialog(inputFrame, "Not a number", "Error", JOptionPane.ERROR_MESSAGE); inputFrame.toFront(); return; } } term = term.replaceAll("(\\W)", "\\\\$1"); term = cbwhole.isSelected() ? (".*\\b" + term + "\\b.*") : (".*" + term + ".*"); if (cbcase.isSelected()) { regPattern = Pattern.compile(term, Pattern.DOTALL); } else { regPattern = Pattern.compile(term, Pattern.CASE_INSENSITIVE | Pattern.DOTALL); } try { boolean isCancelled = false; inputFrame.setVisible(false); resultFrame = new ReferenceHitFrame(title, parent); ThreadPoolExecutor executor = Misc.createThreadPool(); progressIndex = 0; progress = new ProgressMonitor(parent, "Searching...", String.format(FMT_PROGRESS, files.size(), files.size()), 0, files.size()); progress.setMillisToDecideToPopup(100); Debugging.timerReset(); for (int i = 0; i < files.size(); i++) { Misc.isQueueReady(executor, true, -1); executor.execute(new Worker(files.get(i))); if (progress.isCanceled()) { isCancelled = true; break; } } // enforcing thread termination if process has been cancelled if (isCancelled) { executor.shutdownNow(); } else { executor.shutdown(); } // waiting for pending threads to terminate while (!executor.isTerminated()) { if (!isCancelled && progress.isCanceled()) { executor.shutdownNow(); isCancelled = true; } try { Thread.sleep(1); } catch (InterruptedException e) {} } if (isCancelled) { resultFrame.close(); JOptionPane.showMessageDialog(parent, "Search cancelled", "Info", JOptionPane.INFORMATION_MESSAGE); } else { resultFrame.setVisible(true); } } finally { advanceProgress(true); regPattern = null; searchNumber = 0; resultFrame = null; } Debugging.timerShow("Search completed", Debugging.TimeFormat.MILLISECONDS); } // --------------------- End Interface Runnable --------------------- private synchronized void advanceProgress(boolean finished) { if (progress != null) { if (finished) { progressIndex = 0; progress.close(); progress = null; } else { progressIndex++; if (progressIndex % 100 == 0) { progress.setNote(String.format(FMT_PROGRESS, progressIndex, files.size())); } progress.setProgress(progressIndex); } } } private synchronized void addHit(ResourceEntry entry, String name, StructEntry ref) { if (resultFrame != null) { resultFrame.addHit(entry, name, ref); } } private Pattern getPattern() { return regPattern; } private int getSearchNumber() { return searchNumber; } //-------------------------- INNER CLASSES -------------------------- private class Worker implements Runnable { private final ResourceEntry entry; public Worker(ResourceEntry entry) { this.entry = entry; } @Override public void run() { if (entry != null) { AbstractStruct resource = (AbstractStruct)ResourceFactory.getResource(entry); if (resource != null) { List<StructEntry> flatList = resource.getFlatList(); for (int j = 0; j < flatList.size(); j++) { StructEntry searchEntry = (StructEntry)flatList.get(j); // skipping fields located in different parent structures if (structEntry.getParent().getClass() != searchEntry.getParent().getClass()) { continue; } if (structEntry instanceof AbstractCode && structEntry.getClass() == searchEntry.getClass() || searchEntry.getName().equalsIgnoreCase(structEntry.getName())) { boolean hit = false; if (rbexact.isSelected()) { hit = getPattern().matcher(searchEntry.toString()).matches(); } else if (rbless.isSelected()) { hit = getSearchNumber() > ((DecNumber)searchEntry).getValue(); } else if (rbgreater.isSelected()) { hit = getSearchNumber() < ((DecNumber)searchEntry).getValue(); } if (cbnot.isSelected()) { hit = !hit; } if (hit) { AbstractStruct superStruct = resource.getSuperStruct(searchEntry); if (superStruct instanceof Resource || superStruct == null) { addHit(entry, entry.getSearchString(), searchEntry); } else { // creating a path of structures List<String> list = new ArrayList<String>(); while (superStruct != null) { if (superStruct.getSuperStruct() != null) { list.add(0, superStruct.getName()); } superStruct = superStruct.getSuperStruct(); } list.add(0, entry.getSearchString()); StringBuilder sb = new StringBuilder(); for (int k = 0; k < list.size(); k++) { if (k > 0) { sb.append(" -> "); } sb.append(list.get(k)); } addHit(entry, sb.toString(), searchEntry); } } } } } } advanceProgress(false); } } }