blob: f974feb605a6014419f75d9f9d9cf09cd198844c [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.model;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import org.netbeans.editor.BaseDocument;
import org.netbeans.editor.GuardedDocument;
import org.netbeans.modules.editor.NbEditorUtilities;
import org.netbeans.modules.maven.model.pom.Build;
import org.netbeans.modules.maven.model.pom.POMModel;
import org.netbeans.modules.maven.model.pom.POMModelFactory;
import org.netbeans.modules.maven.model.pom.Plugin;
import org.netbeans.modules.maven.model.settings.SettingsModel;
import org.netbeans.modules.maven.model.settings.SettingsModelFactory;
import org.netbeans.modules.xml.xam.ComponentEvent;
import org.netbeans.modules.xml.xam.ComponentListener;
import org.netbeans.modules.xml.xam.Model;
import org.netbeans.modules.xml.xam.ModelSource;
import org.netbeans.modules.xml.xam.dom.AbstractDocumentModel;
import org.openide.awt.StatusDisplayer;
import org.openide.cookies.EditorCookie;
import org.openide.cookies.SaveCookie;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileSystem;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.DataObject;
import org.openide.text.Line;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.UserQuestionException;
import org.openide.util.lookup.AbstractLookup;
import org.openide.util.lookup.InstanceContent;
import org.openide.util.lookup.Lookups;
/**
* Utility class to create ModelSource (environment) for the
* to be created models.
*
* copied from xml.retriever and customized.
* @author mkleint
*/
public class Utilities {
private Utilities() {}
private static final Logger logger = Logger.getLogger(Utilities.class.getName());
/**
*
* @param file
* @param editable
* @param skeleton current content of the document
* @param mimeType
* @return
*/
public static ModelSource createModelSourceForMissingFile(File file, boolean editable, String skeleton, String mimeType) {
try {
BaseDocument doc = new BaseDocument(false, mimeType);
doc.insertString(0, skeleton, null);
InstanceContent ic = new InstanceContent();
Lookup lookup = new AbstractLookup(ic);
ic.add(file);
ic.add(doc);
ModelSource ms = new ModelSource(lookup, editable);
return ms;
} catch (BadLocationException ex) {
Exceptions.printStackTrace(ex);
}
assert false : "Failed to load the model for non-existing file";
return null;
}
private static BaseDocument getDocument(final DataObject modelSourceDataObject) throws IOException {
if (modelSourceDataObject != null && modelSourceDataObject.isValid()) {
EditorCookie ec = modelSourceDataObject.getLookup().lookup(EditorCookie.class);
assert ec != null : "Data object "+modelSourceDataObject.getPrimaryFile().getPath()+" has no editor cookies.";
Document doc;
try {
doc = ec.openDocument();
} catch (UserQuestionException uce) {
// this exception is thrown if the document is to large
// lets just confirm that it is ok
uce.confirmed();
doc = ec.openDocument();
}
if (doc instanceof BaseDocument) {
return (BaseDocument) doc;
} else {
logger.log(Level.FINER, "Got document of unexpected {0} from {1}", new Object[] {doc.getClass(), modelSourceDataObject});
// Replace with a BaseDocument. Mostly useful for unit test.
final BaseDocument doc2 = new GuardedDocument("text/xml");
try {
String str = doc.getText(0, doc.getLength());
doc2.insertString(0, str, null);
} catch (BadLocationException x) {
throw new IOException(x);
}
final Document orig = doc;
doc2.addDocumentListener(new DocumentListener() {
public @Override void insertUpdate(DocumentEvent e) {
try {
orig.insertString(e.getOffset(), doc2.getText(e.getOffset(), e.getLength()), null);
} catch (BadLocationException x) {
assert false : x;
}
}
public @Override void removeUpdate(DocumentEvent e) {
try {
orig.remove(e.getOffset(), e.getLength());
} catch (BadLocationException x) {
assert false : x;
}
}
public @Override void changedUpdate(DocumentEvent e) {}
});
doc2.putProperty(Document.StreamDescriptionProperty, doc.getProperty(Document.StreamDescriptionProperty));
return doc2;
}
} else {
return null;
}
}
/**
* This method could be overridden by the Unit testcase to return a special
* ModelSource object for a FileObject with custom impl of classes added to the lookup.
* This is optional if both getDocument(FO) and createCatalogModel(FO) are overridden.
* @param thisFileObj
* @return
*/
public static ModelSource createModelSource(final FileObject thisFileObj) {
return createModelSource(thisFileObj, null, null);
}
public static ModelSource createModelSource(final FileObject thisFileObj, final DataObject dobject, final BaseDocument document) {
assert thisFileObj != null : "Null file object.";
final File fl = FileUtil.toFile(thisFileObj);
boolean editable = (fl != null);// && thisFileObj.canWrite();
Lookup proxyLookup = Lookups.proxy(new Lookup.Provider() {
@Override
public Lookup getLookup() {
List<Object> items = new ArrayList<Object>();
items.add(thisFileObj);
try {
DataObject dobj = dobject != null ? dobject : DataObject.find(thisFileObj);
items.add(dobj);
BaseDocument doc = document != null ? document : getDocument(dobj);
if (doc != null) {
items.add(doc);
} else {
logger.log(Level.WARNING, "no Document found for {0}", dobj);
}
} catch (IOException x) {
logger.log(Level.SEVERE, x.getMessage());
}
if (fl != null) {
items.add(fl);
}
return Lookups.fixed(items.toArray());
}
});
return new ModelSource(proxyLookup, editable);
}
/**
* attempts to save the document model to disk.
* if model is in transaction, the transaction is ended first,
* then dataobject's SaveCookie is called.
*
* @param model
* @throws java.io.IOException if saving fails.
*/
public static void saveChanges(AbstractDocumentModel<?> model) throws IOException {
if (model.isIntransaction()) {
// the ISE thrown from endTransction is handled in performPOMModelOperations.
model.endTransaction();
}
model.sync();
DataObject dobj = model.getModelSource().getLookup().lookup(DataObject.class);
if (dobj == null) {
final Document doc = model.getModelSource().getLookup().lookup(Document.class);
final File file = model.getModelSource().getLookup().lookup(File.class);
logger.log(Level.FINE, "saving changes in {0}", file);
File parent = file.getParentFile();
FileObject parentFo = FileUtil.toFileObject(parent);
if (parentFo == null) {
parent.mkdirs();
FileUtil.refreshFor(parent);
parentFo = FileUtil.toFileObject(parent);
}
final FileObject fParentFo = parentFo;
if (fParentFo != null) {
FileSystem fs = parentFo.getFileSystem();
fs.runAtomicAction(new FileSystem.AtomicAction() {
public @Override void run() throws IOException {
String text;
try {
text = doc.getText(0, doc.getLength());
} catch (BadLocationException x) {
throw new IOException(x);
}
FileObject fo = fParentFo.getFileObject(file.getName());
if (fo == null) {
fo = fParentFo.createData(file.getName());
}
OutputStream os = fo.getOutputStream();
try {
os.write(text.getBytes("UTF-8"));
} finally {
os.close();
}
}
});
} else {
//TODO report
}
} else {
SaveCookie save = dobj.getLookup().lookup(SaveCookie.class);
if (save != null) {
logger.log(Level.FINE, "saving changes in {0}", dobj);
save.save();
} else {
logger.log(Level.FINE, "no changes in {0} where modified={1}", new Object[] {dobj, dobj.isModified()});
}
}
}
/**
* performs model modifying operations on top of the POM model. After modifications,
* the model is persisted to file.
* @param pomFileObject
* @param operations
*/
public static void performPOMModelOperations(final FileObject pomFileObject, List<? extends ModelOperation<POMModel>> operations) {
assert pomFileObject != null;
ModelSource source = Utilities.createModelSource(pomFileObject);
performPOMModelOperations(source, operations);
}
/**
* performs model modifying operations on top of the POM model. After modifications,
* the model is persisted to file.
* @param source
* @param operations
* @since 1.36
*/
public static void performPOMModelOperations(final ModelSource source, List<? extends ModelOperation<POMModel>> operations) {
assert source != null;
assert operations != null;
if (source.getLookup().lookup(BaseDocument.class) == null) {
logger.log(Level.WARNING, "#193187: no Document associated with {0}", getPathFromSource(source));
return;
}
POMModel model = POMModelFactory.getDefault().getModel(source);
if (model != null) {
try {
model.sync();
if (Model.State.VALID != model.getState()) {
StatusDisplayer.getDefault().setStatusText(NbBundle.getMessage(Utilities.class, "ERR_POM", NbBundle.getMessage(Utilities.class,"ERR_INVALID_MODEL")), StatusDisplayer.IMPORTANCE_ERROR_HIGHLIGHT).clear(10000);
return;
}
if (!model.startTransaction()) {
logger.log(Level.WARNING, "Could not start transaction on {0}", getPathFromSource(source));
return;
}
final AtomicBoolean modified = new AtomicBoolean();
ComponentListener listener = new ComponentListener() {
private void change(ComponentEvent evt) {
logger.log(Level.FINE, "{0}: {1}", new Object[] {getPathFromSource(source), evt});
modified.set(true);
}
@Override public void valueChanged(ComponentEvent evt) {
change(evt);
}
@Override public void childrenAdded(ComponentEvent evt) {
change(evt);
}
@Override public void childrenDeleted(ComponentEvent evt) {
change(evt);
}
};
model.addComponentListener(listener);
try {
for (ModelOperation<POMModel> op : operations) {
op.performOperation(model);
}
model.endTransaction();
} finally {
model.removeComponentListener(listener);
}
if (modified.get()) {
Utilities.saveChanges(model);
} else {
logger.log(Level.FINE, "no changes recorded in {0}", getPathFromSource(source));
}
} catch (IOException ex) {
StatusDisplayer.getDefault().setStatusText(NbBundle.getMessage(Utilities.class, "ERR_POM", ex.getLocalizedMessage()), StatusDisplayer.IMPORTANCE_ERROR_HIGHLIGHT).clear(10000);
logger.log(Level.INFO, "Cannot write POM", ex);
// Exceptions.printStackTrace(ex);
} catch (IllegalStateException ex) {
StatusDisplayer.getDefault().setStatusText(NbBundle.getMessage(Utilities.class, "ERR_POM", ex.getLocalizedMessage()), StatusDisplayer.IMPORTANCE_ERROR_HIGHLIGHT).clear(10000);
logger.log(Level.INFO, "Cannot write POM", ex);
} finally {
if (model.isIntransaction()) {
model.rollbackTransaction();
}
}
} else {
logger.log(Level.WARNING, "Cannot create model from current content of {0}", getPathFromSource(source));
}
}
private static String getPathFromSource(ModelSource source) {
File f = source.getLookup().lookup(File.class);
if (f != null) {
return f.getAbsolutePath();
}
DataObject dob = source.getLookup().lookup(DataObject.class);
if (dob != null) {
return dob.getPrimaryFile().getPath();
}
return source.toString();
}
/**
* performs model modifying operations on top of the settings.xml model. After modifications,
* the model is persisted to file.
* @param settingsFileObject
* @param operations
*/
public static void performSettingsModelOperations(FileObject settingsFileObject, List<? extends ModelOperation<SettingsModel>> operations) {
assert settingsFileObject != null;
assert operations != null;
ModelSource source = Utilities.createModelSource(settingsFileObject);
SettingsModel model = SettingsModelFactory.getDefault().getModel(source);
if (model != null) {
try {
model.sync();
if (Model.State.VALID != model.getState()) {
StatusDisplayer.getDefault().setStatusText(NbBundle.getMessage(Utilities.class, "ERR_SETTINGS", NbBundle.getMessage(Utilities.class,"ERR_INVALID_MODEL")), StatusDisplayer.IMPORTANCE_ERROR_HIGHLIGHT).clear(10000);
return;
}
if (!model.startTransaction()) {
logger.log(Level.WARNING, "Could not start transaction on {0}", settingsFileObject);
return;
}
for (ModelOperation<SettingsModel> op : operations) {
op.performOperation(model);
}
model.endTransaction();
Utilities.saveChanges(model);
} catch (IOException ex) {
StatusDisplayer.getDefault().setStatusText(NbBundle.getMessage(Utilities.class, "ERR_SETTINGS", ex.getLocalizedMessage()), StatusDisplayer.IMPORTANCE_ERROR_HIGHLIGHT).clear(10000);
Logger.getLogger(Utilities.class.getName()).log(Level.INFO, "Cannot write settings.xml", ex);
} catch (IllegalStateException ex) {
StatusDisplayer.getDefault().setStatusText(NbBundle.getMessage(Utilities.class, "ERR_SETTINGS", ex.getLocalizedMessage()), StatusDisplayer.IMPORTANCE_ERROR_HIGHLIGHT).clear(10000);
Logger.getLogger(Utilities.class.getName()).log(Level.INFO, "Cannot write settings.xml", ex);
} finally {
if (model.isIntransaction()) {
model.rollbackTransaction();
}
}
} else {
//TODO report error.. what is the error?
}
}
/**
* Opens pom at given offset.
* @param model the model to open
* @param offset position to open at
* @since 1.44
*/
public static void openAtPosition(final POMModel model, final int offset) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
try {
Line line = NbEditorUtilities.getLine(model.getBaseDocument(), offset, false);
line.show(Line.ShowOpenType.OPEN, Line.ShowVisibilityType.FOCUS);
} catch (IndexOutOfBoundsException e) {
logger.log(Level.FINE, "document changed", e);
}
}
});
}
}