blob: adda19cc6eeb9817981aadc41eed502bf0de7816 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.netbeans.modules.maven.navigator;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.beans.PropertyVetoException;
import java.io.File;
import java.io.IOException;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.text.JTextComponent;
import javax.xml.namespace.QName;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.model.Model;
import org.apache.maven.model.Parent;
import org.apache.maven.model.building.ModelBuildingException;
import org.apache.maven.model.building.ModelProblem;
import org.netbeans.api.annotations.common.StaticResource;
import org.netbeans.modules.editor.NbEditorUtilities;
import org.netbeans.modules.maven.api.Constants;
import org.netbeans.modules.maven.embedder.EmbedderFactory;
import org.netbeans.modules.maven.embedder.MavenEmbedder;
import org.netbeans.modules.maven.model.pom.*;
import org.netbeans.modules.maven.navigator.POMModelVisitor.POMCutHolder;
import org.netbeans.modules.maven.spi.nodes.NodeUtils;
import org.netbeans.modules.xml.xam.ModelSource;
import org.openide.cookies.EditorCookie;
import org.openide.explorer.ExplorerManager;
import org.openide.explorer.view.BeanTreeView;
import org.openide.filesystems.FileChangeAdapter;
import org.openide.filesystems.FileEvent;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.Children;
import org.openide.nodes.Node;
import org.openide.util.*;
import org.openide.windows.TopComponent;
import org.w3c.dom.NodeList;
/**
*
* @author mkleint
*/
public class POMModelPanel extends javax.swing.JPanel implements ExplorerManager.Provider, Runnable, CaretListener {
private static final @StaticResource String FILTER_HIDE = "org/netbeans/modules/maven/navigator/filterHideFields.gif";
private static final @StaticResource String SORT_ALPHA = "org/netbeans/modules/maven/navigator/sortAlpha.png";
private static final Logger LOG = Logger.getLogger(POMModelPanel.class.getName());
private static final String NAVIGATOR_SHOW_UNDEFINED = "navigator.showUndefined"; //NOI18N
private static final String NAVIGATOR_SORT_LISTS = "navigator.sortLists"; //NOI18N
private final transient ExplorerManager explorerManager = new ExplorerManager();
private final BeanTreeView treeView;
private DataObject current;
private Reference<JTextComponent> currentComponent;
private int currentDot = -1;
private static final RequestProcessor RP = new RequestProcessor(POMModelPanel.class.getName(), 2);
private final RequestProcessor.Task caretTask = RP.create(new Runnable() {
@Override
public void run() {
if (currentDot != -1) {
updateCaret(currentDot);
}
}
});
private final RequestProcessor.Task showTask = RP.create(this);
private final FileChangeAdapter adapter = new FileChangeAdapter(){
@Override
public void fileChanged(FileEvent fe) {
showWaitNode();
showTask.schedule(0);
}
};
private final TapPanel filtersPanel;
private final Configuration configuration;
/** Creates new form POMInheritancePanel */
public POMModelPanel() {
initComponents();
configuration = new Configuration();
boolean filterIncludeUndefined = NbPreferences.forModule(POMModelPanel.class).getBoolean(NAVIGATOR_SHOW_UNDEFINED, true);
boolean sortLists = NbPreferences.forModule(POMModelPanel.class).getBoolean(NAVIGATOR_SORT_LISTS, false);
configuration.setFilterUndefined(filterIncludeUndefined);
configuration.setSortLists(sortLists);
treeView = (BeanTreeView)jScrollPane1;
// filters
filtersPanel = new TapPanel();
filtersPanel.setOrientation(TapPanel.DOWN);
// tooltip
KeyStroke toggleKey = KeyStroke.getKeyStroke(KeyEvent.VK_T,
Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
String keyText = Utilities.keyToString(toggleKey);
filtersPanel.setToolTipText(NbBundle.getMessage(POMModelPanel.class, "TIP_TapPanel", keyText)); //NOI18N
JComponent buttons = createFilterButtons();
buttons.setBorder(BorderFactory.createEmptyBorder(0, 5, 5, 0));
filtersPanel.add(buttons);
if( "Aqua".equals(UIManager.getLookAndFeel().getID()) ) {
filtersPanel.setBackground(UIManager.getColor("NbExplorerView.background"));//NOI18N
}
add(filtersPanel, BorderLayout.SOUTH);
}
static void selectByNode(Node nd, int layer) {
selectByNode(nd, null, layer, null);
}
private static void selectByNode(Node nd, String elementName, int layer, String elementValue) {
if (nd == null) {
return;
}
POMModelVisitor.POMCutHolder holder = nd.getLookup().lookup(POMModelVisitor.POMCutHolder.class);
if (holder != null) {
Object[] objs = holder.getCutValues();
if (layer >= objs.length) {
return;
}
if (objs[layer] != null && objs[layer] instanceof POMComponent) {
POMComponent pc = (POMComponent) objs[layer];
int pos;
if (elementName != null) {
QName qn = POMQName.createQName(elementName, pc.getModel().getPOMQNames().isNSAware());
NodeList nl = pc.getPeer().getElementsByTagName(qn.getLocalPart());
if (nl != null && nl.getLength() > 0) {
if (nl.getLength() == 1) {
pos = pc.getModel().getAccess().findPosition(nl.item(0));
} else {
//#211429
pos = pc.getModel().getAccess().findPosition(nl.item(0));
for (int i = 0; i < nl.getLength(); i++) {
org.w3c.dom.Node candidate = nl.item(i);
if (candidate instanceof org.w3c.dom.Element) {
org.w3c.dom.Element candidEl = (org.w3c.dom.Element)candidate;
if (elementValue != null && elementValue.equals(getText(candidEl))) {
pos = pc.getModel().getAccess().findPosition(candidate);
break;
}
}
}
}
} else {
pos = -1;
}
} else {
pos = pc.getModel().getAccess().findPosition(pc.getPeer());
}
if (pos != -1) {
select(nd, pos, layer);
}
} else if (objs[layer] != null && elementName == null) {
String name = getElementNameFromNode(nd);
selectByNode(nd.getParentNode(), name, layer, objs[layer].toString());
}
}
}
// a custom method for getting the text of the element getTextContent() doesn't work/not implemented.
private static String getText(org.w3c.dom.Element el) {
NodeList nl = el.getChildNodes();
if (nl.getLength() > 0) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < nl.getLength(); i++) {
org.w3c.dom.Node nd = nl.item(i);
if (nd instanceof org.w3c.dom.Text) {
org.w3c.dom.Text txt = (org.w3c.dom.Text)nd;
String s = txt.getNodeValue();
if (s != null) {
sb.append(s);
}
}
}
return sb.toString();
}
return null;
}
private static void select(final Node node, final int pos, final int layer) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
POMCutHolder hold = node.getLookup().lookup(POMCutHolder.class);
POMModel[] models = hold.getSource();
if (models.length <= layer) {
return;
}
POMModel mdl = models[layer];
DataObject dobj = mdl.getModelSource().getLookup().lookup(DataObject.class);
if (dobj == null) {
return;
}
try {
dobj = DataObject.find(NodeUtils.readOnlyLocalRepositoryFile(dobj.getPrimaryFile()));
} catch (DataObjectNotFoundException x) {
LOG.log(Level.INFO, null, x);
}
EditorCookie.Observable ec = dobj.getLookup().lookup(EditorCookie.Observable.class);
if (ec == null) {
return;
}
JEditorPane[] panes = ec.getOpenedPanes();
if (panes != null && panes.length > 0) {
// editor already opened, so just select
JTextComponent component = panes[0];
try {
component.setCaretPosition(pos);
} catch (IllegalArgumentException iae) {
//#165408
// swallow if the position is out of bounds,
// the model and document was not properly synced yet
}
TopComponent tc = NbEditorUtilities.getOuterTopComponent(component);
if (!tc.isVisible()) {
tc.requestVisible();
}
} else {
// editor not opened yet
ec.open();
try {
ec.openDocument(); //wait to editor to open
panes = ec.getOpenedPanes();
if (panes != null && panes.length > 0) {
JTextComponent component = panes[0];
try {
component.setCaretPosition(pos);
} catch (IllegalArgumentException iae) {
//#165408
// swallow if the position is out of bounds,
// the model and document was not properly synced yet
}
}
} catch (IOException ioe) {
}
}
}
});
}
@Override
public ExplorerManager getExplorerManager() {
return explorerManager;
}
void navigate(DataObject d) {
cleanup();
current = d;
current.getPrimaryFile().addFileChangeListener(adapter);
showWaitNode();
showTask.schedule(0);
}
void cleanup() {
if (current != null) {
current.getPrimaryFile().removeFileChangeListener(adapter);
}
JTextComponent cc = currentComponent != null ? currentComponent.get() : null;
if (cc != null) {
cc.removeCaretListener(this);
}
}
@Override
public void run() {
DataObject currentFile = current;
//#164852 somehow a folder dataobject slipped in, test mimetype to avoid that.
// the root cause of the problem is unknown though
if (currentFile != null && Constants.POM_MIME_TYPE.equals(currentFile.getPrimaryFile().getMIMEType())) { //NOI18N
File file = FileUtil.toFile(currentFile.getPrimaryFile());
//now attach the listener to the textcomponent
final EditorCookie.Observable ec = currentFile.getLookup().lookup(EditorCookie.Observable.class);
if (ec == null) {
//how come?
return;
}
// can be null for stuff in jars?
if (file != null) {
try {
MavenEmbedder embedder = EmbedderFactory.getProjectEmbedder();
List<Project> prjs = new ArrayList<Project>();
List<POMModel> mdls = new ArrayList<POMModel>();
POMQNames names = null;
for (Model m : embedder.createModelLineage(file)) {
File pom = m.getPomFile();
if (pom == null) {
if (m.getArtifactId() == null) { // normal for superpom
continue;
}
Parent parent = m.getParent();
String groupId = m.getGroupId();
if (groupId == null && parent != null) {
groupId = parent.getGroupId();
}
assert groupId != null;
String version = m.getVersion();
if (version == null && parent != null) {
version = parent.getVersion();
}
assert version != null;
Artifact a = embedder.createArtifact(groupId, m.getArtifactId(), version, m.getPackaging());
try {
embedder.resolve(a, Collections.<ArtifactRepository>emptyList(), embedder.getLocalRepository());
} catch (Exception x) {
LOG.log(Level.INFO, "could not resolve " + a, x);
}
pom = a.getFile();
if (pom == null) {
LOG.log(Level.WARNING, "#163933: null pom for {0}", m.getId());
continue;
}
}
FileUtil.refreshFor(pom);
FileObject fo = FileUtil.toFileObject(pom);
if (fo != null) {
ModelSource ms = org.netbeans.modules.maven.model.Utilities.createModelSource(fo);
POMModel mdl = POMModelFactory.getDefault().createFreshModel(ms);
if (mdl != null) {
prjs.add(mdl.getProject());
mdls.add(mdl);
names = mdl.getPOMQNames();
} else {
LOG.log(Level.WARNING, "no model for {0}", pom);
}
} else {
LOG.log(Level.WARNING, "no fileobject for {0}", pom);
}
}
if (names == null) { // #199698
return;
}
final POMModelVisitor.POMCutHolder hold = new POMModelVisitor.SingleObjectCH(mdls.toArray(new POMModel[0]), names, names.PROJECT, Project.class, configuration);
for (Project p : prjs) {
hold.addCut(p);
}
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
treeView.setRootVisible(false);
explorerManager.setRootContext(hold.createNode());
}
});
} catch (final ModelBuildingException ex) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
treeView.setRootVisible(true);
explorerManager.setRootContext(createErrorNode(ex));
}
});
}
} else {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
treeView.setRootVisible(false);
explorerManager.setRootContext(createEmptyNode());
}
});
}
try {
ec.openDocument(); //wait to editor to open
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
JEditorPane[] panes = ec.getOpenedPanes();
if (panes != null && panes.length > 0) {
// editor already opened, so just select
JTextComponent component = panes[0];
component.removeCaretListener(POMModelPanel.this);
component.addCaretListener(POMModelPanel.this);
currentComponent = new WeakReference<JTextComponent>(component);
}
}
} );
}
}
/**
*
*/
void release() {
if (current != null) {
current.getPrimaryFile().removeFileChangeListener(adapter);
}
current = null;
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
treeView.setRootVisible(false);
explorerManager.setRootContext(createEmptyNode());
}
});
}
/**
*
*/
public void showWaitNode() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
treeView.setRootVisible(true);
explorerManager.setRootContext(createWaitNode());
}
});
}
private JComponent createFilterButtons() {
Box box = new Box(BoxLayout.X_AXIS);
box.setBorder(new EmptyBorder(1, 2, 3, 5));
// configure toolbar
JToolBar toolbar = new JToolBar(JToolBar.HORIZONTAL) {
@Override
protected void paintComponent(Graphics g) {
}
};
toolbar.setFloatable(false);
toolbar.setRollover(true);
toolbar.setBorderPainted(false);
toolbar.setBorder(BorderFactory.createEmptyBorder());
toolbar.setOpaque(false);
toolbar.setFocusable(false);
JToggleButton tg1 = new JToggleButton(new ShowUndefinedAction());
tg1.setSelected(configuration.isFilterUndefined());
toolbar.add(tg1);
JToggleButton tg2 = new JToggleButton(new SortListsAction());
tg2.setSelected(configuration.isSortLists());
toolbar.add(tg2);
Dimension space = new Dimension(3, 0);
toolbar.addSeparator(space);
box.add(toolbar);
return box;
}
private static String getElementNameFromNode(Node childNode) {
String qnName;
QName qn = childNode.getLookup().lookup(QName.class);
if (qn == null) {
POMQName pqn = childNode.getLookup().lookup(POMQName.class);
if (pqn != null) {
qn = pqn.getQName();
}
}
if (qn != null) {
qnName = qn.getLocalPart();
} else {
//properties
qnName = childNode.getLookup().lookup(String.class);
}
return qnName;
}
/** This method is called from within the constructor to
* initialize the form.
* WARNING: Do NOT modify this code. The content of this method is
* always regenerated by the Form Editor.
*/
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
private void initComponents() {
jScrollPane1 = new BeanTreeView();
setLayout(new java.awt.BorderLayout());
add(jScrollPane1, java.awt.BorderLayout.CENTER);
}// </editor-fold>//GEN-END:initComponents
// Variables declaration - do not modify//GEN-BEGIN:variables
private javax.swing.JScrollPane jScrollPane1;
// End of variables declaration//GEN-END:variables
private static Node createWaitNode() {
AbstractNode an = new AbstractNode(Children.LEAF);
an.setIconBaseWithExtension("org/netbeans/modules/maven/navigator/wait.gif"); //NOI18N
an.setDisplayName(NbBundle.getMessage(POMInheritancePanel.class, "LBL_Wait"));
return an;
}
private static Node createEmptyNode() {
AbstractNode an = new AbstractNode(Children.LEAF);
return an;
}
static Node createErrorNode(ModelBuildingException x) {
AbstractNode an = new AbstractNode(Children.LEAF);
StringBuilder b = new StringBuilder();
for (ModelProblem p : x.getProblems()) {
if (b.length() > 0) {
b.append("; ");
}
b.append(p.getMessage());
}
an.setDisplayName(b.toString());
return an;
}
/**
* returns true if the value is defined in current pom. Assuming the first index is the
* current pom and the next value is it's parent, etc.
*/
static boolean isValueDefinedInCurrent(Object[] values) {
return values[0] != null;
}
/**
* returns true if the value is defined in current pom
* and one of the parent poms as well.
*/
static boolean overridesParentValue(Object[] values) {
if (values.length <= 1) {
return false;
}
boolean curr = values[0] != null;
boolean par = false;
for (int i = 1; i < values.length; i++) {
if (values[i] != null) {
par = true;
break;
}
}
return curr && par;
}
/**
* returns true if the value is defined in in any pom. Assuming the first index is the
* current pom and the next value is it's parent, etc.
*/
static boolean definesValue(Object[] values) {
for (int i = 0; i < values.length; i++) {
if (values[i] != null) {
return true;
}
}
return false;
}
/**
* @return level where the last value is defined, 0 - current file, 1 - it's parent,... -1 not present..
*
*/
static int currentValueDepth(Object[] values) {
for (int i = 0; i < values.length; i++) {
if (values[i] != null) {
return i;
}
}
return -1;
}
/**
* gets the first defined value from the list. Assuming the first index is the
* current pom and the next value is it's parent, etc.
*/
static String getValidValue(String[] values) {
for (int i = 0; i < values.length; i++) {
if (values[i] != null) {
return values[i];
}
}
return null;
}
@Override
public void caretUpdate(CaretEvent e) {
JTextComponent cc = currentComponent != null ? currentComponent.get() : null;
if (e.getSource() != cc) {
((JTextComponent)e.getSource()).removeCaretListener(this);
//just a double check we do't get a persistent leak here..
return;
}
currentDot = e.getDot();
caretTask.schedule(1000);
}
private void updateCaret(int caret) {
POMCutHolder pch = getExplorerManager().getRootContext().getLookup().lookup(POMCutHolder.class);
if (pch != null) {
POMComponent pc = (POMComponent) pch.getSource()[0].findComponent(caret);
Stack<POMComponent> stack = new Stack<POMComponent>();
while (pc != null) {
stack.push(pc);
pc = pc.getParent();
}
Node currentNode = getExplorerManager().getRootContext();
if (stack.empty()) {
return;
}
//pop the project root.
POMComponent currentpc = stack.pop();
boolean found = false;
while (!stack.empty()) {
currentpc = stack.pop();
found = false;
Node[] childs = currentNode.getChildren().getNodes(true);
Class listClass = null;
if (currentpc instanceof ModelList) {
ModelList lst = (ModelList)currentpc;
listClass = lst.getListClass();
}
for (Node childNode : childs) {
POMCutHolder holder = childNode.getLookup().lookup(POMCutHolder.class);
Object currentObj = holder.getCutValues()[0];
if (currentObj instanceof POMComponent) {
if (currentObj == currentpc) {
treeView.expandNode(currentNode);
currentNode = childNode;
found = true;
break;
}
}
if (currentObj instanceof String) {
String qnName = getElementNameFromNode(childNode);
if (qnName == null || (!(currentpc instanceof POMExtensibilityElement))) {
//TODO can be also string in lookup;
continue;
}
POMExtensibilityElement exEl = (POMExtensibilityElement) currentpc;
if (exEl.getQName().getLocalPart().equals(qnName)) {
treeView.expandNode(currentNode);
currentNode = childNode;
found = true;
break;
}
}
if (currentObj != null && holder instanceof POMModelVisitor.ListObjectCH
&& listClass != null) {
POMModelVisitor.ListObjectCH loh = (POMModelVisitor.ListObjectCH)holder;
if (loh.getListClass().equals(listClass)) {
treeView.expandNode(currentNode);
currentNode = childNode;
found = true;
break;
}
}
}
if (!found) {
break;
}
}
if (found) {
try {
getExplorerManager().setSelectedNodes(new Node[]{currentNode});
} catch (PropertyVetoException ex) {
Exceptions.printStackTrace(ex);
}
}
}
}
private class ShowUndefinedAction extends AbstractAction {
public ShowUndefinedAction() {
putValue(SMALL_ICON, ImageUtilities.image2Icon(ImageUtilities.loadImage(FILTER_HIDE))); //NOI18N
putValue(SHORT_DESCRIPTION, org.openide.util.NbBundle.getMessage(POMModelPanel.class, "DESC_FilterUndefined"));
}
@Override
public void actionPerformed(ActionEvent e) {
boolean current = configuration.isFilterUndefined();
configuration.setFilterUndefined(!current);
NbPreferences.forModule(POMModelPanel.class).putBoolean( NAVIGATOR_SHOW_UNDEFINED, !current);
}
}
private class SortListsAction extends AbstractAction {
public SortListsAction() {
putValue(SMALL_ICON, ImageUtilities.image2Icon(ImageUtilities.loadImage(SORT_ALPHA))); //NOI18N
putValue(SHORT_DESCRIPTION, org.openide.util.NbBundle.getMessage(POMModelPanel.class, "DESC_SortLists"));
}
@Override
public void actionPerformed(ActionEvent e) {
boolean current = configuration.isSortLists();
configuration.setSortLists(!current);
NbPreferences.forModule(POMModelPanel.class).putBoolean( NAVIGATOR_SORT_LISTS, !current);
}
}
static class Configuration {
private boolean filterUndefined;
private boolean sortLists;
private final java.beans.PropertyChangeSupport propertyChangeSupport = new java.beans.PropertyChangeSupport(this);
public static final String PROP_FILTERUNDEFINED = "filterUndefined"; //NOI18N
public static final String PROP_SORT_LISTS = "sortLists";
/**
* Get the value of filterUndefined
*
* @return the value of filterUndefined
*/
public boolean isFilterUndefined() {
return filterUndefined;
}
/**
* Set the value of filterUndefined
*
* @param filterUndefined new value of filterUndefined
*/
public void setFilterUndefined(boolean filterUndefined) {
boolean oldFilterUndefined = this.filterUndefined;
this.filterUndefined = filterUndefined;
propertyChangeSupport.firePropertyChange(PROP_FILTERUNDEFINED, oldFilterUndefined, filterUndefined);
}
public boolean isSortLists() {
return sortLists;
}
public void setSortLists(boolean sortLists) {
boolean old = this.sortLists;
this.sortLists = sortLists;
propertyChangeSupport.firePropertyChange(PROP_SORT_LISTS, old, sortLists);
}
/**
* Add PropertyChangeListener.
*
* @param listener
*/
public void addPropertyChangeListener(java.beans.PropertyChangeListener listener) {
propertyChangeSupport.addPropertyChangeListener(listener);
}
/**
* Remove PropertyChangeListener.
*
* @param listener
*/
public void removePropertyChangeListener(java.beans.PropertyChangeListener listener) {
propertyChangeSupport.removePropertyChangeListener(listener);
}
}
}