/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org)
*
* 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 org.jkiss.dbeaver.ui.editors.text;
import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension4;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.core.DBeaverUI;
import org.jkiss.dbeaver.ui.editors.EditorUtils;
import org.jkiss.dbeaver.ui.editors.IPersistentStorage;
import org.jkiss.dbeaver.ui.editors.ProjectFileEditorInput;
import org.jkiss.dbeaver.utils.ContentUtils;
import org.jkiss.dbeaver.utils.GeneralUtils;
import org.jkiss.utils.IOUtils;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction;
/**
* FileRefDocumentProvider
*/
public class FileRefDocumentProvider extends BaseTextDocumentProvider {
private static final Log log = Log.getLog(FileRefDocumentProvider.class);
private static final int DEFAULT_BUFFER_SIZE = 10000;
public FileRefDocumentProvider()
{
}
protected IEditorInput createNewEditorInput(IFile newFile)
{
return new ProjectFileEditorInput(newFile);
}
@Override
protected Document createDocument(Object element) throws CoreException
{
Document document = createEmptyDocument();
IStorage storage = EditorUtils.getStorageFromInput(element);
if (storage != null) {
if (setDocumentContent(document, storage)) {
setupDocument(document);
return document;
}
}
File file = EditorUtils.getLocalFileFromInput(element);
if (file != null) {
try (InputStream stream = new FileInputStream(file)) {
setDocumentContent(document, stream, null);
setupDocument(document);
return document;
} catch (IOException e) {
throw new CoreException(GeneralUtils.makeExceptionStatus(e));
}
}
throw new IllegalArgumentException("Project document provider supports only editor inputs which provides IStorage facility");
}
protected void setupDocument(IDocument document)
{
}
@Override
public boolean isReadOnly(Object element)
{
IStorage storage = EditorUtils.getStorageFromInput(element);
if (storage != null) {
return storage.isReadOnly();
}
File file = EditorUtils.getLocalFileFromInput(element);
if (file != null) {
return !file.isFile();
}
return super.isReadOnly(element);
}
@Override
public boolean isModifiable(Object element)
{
return !isReadOnly(element);
}
@Override
public boolean isDeleted(Object element)
{
IStorage storage = EditorUtils.getStorageFromInput(element);
if (storage instanceof IResource) {
return !((IResource)storage).exists();
}
File file = EditorUtils.getLocalFileFromInput(element);
if (file != null) {
return !file.exists();
}
return super.isDeleted(element);
}
@Override
protected void doSaveDocument(IProgressMonitor monitor, Object element, IDocument document, boolean overwrite) throws CoreException
{
try {
IStorage storage = EditorUtils.getStorageFromInput(element);
File localFile = null;
if (storage == null) {
localFile = EditorUtils.getLocalFileFromInput(element);
if (localFile == null) {
throw new DBException("Can't obtain file from editor input");
}
}
String encoding = (storage instanceof IEncodedStorage ? ((IEncodedStorage)storage).getCharset() : GeneralUtils.UTF8_ENCODING);
Charset charset = Charset.forName(encoding);
CharsetEncoder encoder = charset.newEncoder();
encoder.onMalformedInput(CodingErrorAction.REPLACE);
encoder.onUnmappableCharacter(CodingErrorAction.REPORT);
byte[] bytes;
ByteBuffer byteBuffer = encoder.encode(CharBuffer.wrap(document.get()));
if (byteBuffer.hasArray()) {
bytes = byteBuffer.array();
} else {
bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
}
InputStream stream = new ByteArrayInputStream(bytes, 0, byteBuffer.limit());
if (storage instanceof IFile) {
IFile file = (IFile)storage;
if (file.exists()) {
// inform about the upcoming content change
fireElementStateChanging(element);
try {
file.setContents(stream, true, true, monitor);
} catch (CoreException x) {
// inform about failure
fireElementStateChangeFailed(element);
throw x;
} catch (RuntimeException x) {
// inform about failure
fireElementStateChangeFailed(element);
throw x;
}
} else {
try {
monitor.beginTask("Save file '" + file.getName() + "'", 2000);
//ContainerCreator creator = new ContainerCreator(file.getWorkspace(), file.getParent().getFullPath());
//creator.createContainer(new SubProgressMonitor(monitor, 1000));
file.create(stream, false, monitor);
}
finally {
monitor.done();
}
}
} else if (storage instanceof IPersistentStorage) {
monitor.beginTask("Save document", 1);
((IPersistentStorage) storage).setContents(monitor, stream);
} else if (localFile != null) {
try (OutputStream os = new FileOutputStream(localFile)) {
IOUtils.copyStream(stream, os);
}
} else {
throw new DBException("Storage [" + storage + "] doesn't support save");
}
} catch (Exception e) {
if (e instanceof CoreException) {
throw (CoreException) e;
} else {
throw new CoreException(GeneralUtils.makeExceptionStatus(e));
}
}
}
protected boolean setDocumentContent(IDocument document, IStorage storage) throws CoreException
{
try (InputStream contentStream = storage.getContents()) {
String encoding = (storage instanceof IEncodedStorage ? ((IEncodedStorage)storage).getCharset() : GeneralUtils.getDefaultFileEncoding());
setDocumentContent(document, contentStream, encoding);
} catch (IOException e) {
throw new CoreException(GeneralUtils.makeExceptionStatus(e));
}
return true;
}
protected void setDocumentContent(IDocument document, InputStream contentStream, String encoding) throws IOException
{
Reader in = null;
try {
if (encoding == null) {
encoding = GeneralUtils.UTF8_ENCODING;
}
in = new BufferedReader(new InputStreamReader(contentStream, encoding), DEFAULT_BUFFER_SIZE);
StringBuilder buffer = new StringBuilder(DEFAULT_BUFFER_SIZE);
char[] readBuffer = new char[2048];
int n = in.read(readBuffer);
while (n > 0) {
buffer.append(readBuffer, 0, n);
n = in.read(readBuffer);
}
document.set(buffer.toString());
} finally {
if (in != null) {
ContentUtils.close(in);
} else {
ContentUtils.close(contentStream);
}
}
}
protected long computeModificationStamp(IResource resource)
{
long modificationStamp = resource.getModificationStamp();
IPath path = resource.getLocation();
if (path == null) {
return modificationStamp;
}
modificationStamp = path.toFile().lastModified();
return modificationStamp;
}
protected void refreshFile(IFile file) throws CoreException {
refreshFile(file, getProgressMonitor());
}
@Override
protected ElementInfo createElementInfo(Object element) throws CoreException
{
if (element instanceof IEditorInput) {
IEditorInput input = (IEditorInput) element;
IStorage storage = EditorUtils.getStorageFromInput(input);
if (storage instanceof IFile) {
IFile file = (IFile)storage;
try {
refreshFile(file);
} catch (CoreException x) {
log.warn("Can't refresh file", x);
}
IDocument d;
IStatus s = null;
try {
d = createDocument(element);
} catch (CoreException x) {
log.warn("Can't create document", x);
s = x.getStatus();
d = createEmptyDocument();
}
// Set the initial line delimiter
String initialLineDelimiter = GeneralUtils.getDefaultLineSeparator();
if (initialLineDelimiter != null) {
((IDocumentExtension4) d).setInitialLineDelimiter(initialLineDelimiter);
}
IAnnotationModel m = createAnnotationModel(element);
FileSynchronizer f = new FileSynchronizer(input);
f.install();
FileInfo info = new FileInfo(d, m, f);
info.modificationStamp = computeModificationStamp(file);
info.fStatus = s;
return info;
}
}
return super.createElementInfo(element);
}
@Override
protected void disposeElementInfo(Object element, ElementInfo info)
{
if (info instanceof FileInfo) {
FileInfo fileInfo = (FileInfo) info;
if (fileInfo.fileSynchronizer != null) {
fileInfo.fileSynchronizer.uninstall();
}
}
super.disposeElementInfo(element, info);
}
protected void refreshFile(IFile file, IProgressMonitor monitor) throws CoreException {
if (file != null) {
try {
file.refreshLocal(IResource.DEPTH_INFINITE, monitor);
} catch (OperationCanceledException x) {
// do nothing
}
}
}
/**
* Updates the element info to a change of the file content and sends out
* appropriate notifications.
*
* @param fileEditorInput the input of an text editor
*/
protected void handleElementContentChanged(IEditorInput fileEditorInput)
{
FileInfo info = (FileInfo) getElementInfo(fileEditorInput);
if (info == null) {
return;
}
IStorage storage = EditorUtils.getStorageFromInput(fileEditorInput);
if (storage instanceof IFile) {
IFile file = (IFile)storage;
IDocument document = createEmptyDocument();
IStatus status = null;
try {
try {
refreshFile(file);
} catch (CoreException x) {
log.error("handleElementContentChanged", x);
}
setDocumentContent(document, file);
} catch (CoreException x) {
status = x.getStatus();
}
String newContent = document.get();
if (!newContent.equals(info.fDocument.get())) {
// set the new content and fire content related events
fireElementContentAboutToBeReplaced(fileEditorInput);
removeUnchangedElementListeners(fileEditorInput, info);
info.fDocument.removeDocumentListener(info);
info.fDocument.set(newContent);
info.fCanBeSaved = false;
info.modificationStamp = computeModificationStamp(file);
info.fStatus = status;
addUnchangedElementListeners(fileEditorInput, info);
fireElementContentReplaced(fileEditorInput);
} else {
removeUnchangedElementListeners(fileEditorInput, info);
// fires only the dirty state related event
info.fCanBeSaved = false;
info.modificationStamp = computeModificationStamp(file);
info.fStatus = status;
addUnchangedElementListeners(fileEditorInput, info);
fireElementDirtyStateChanged(fileEditorInput, false);
}
}
}
/**
* Sends out the notification that the file serving as document input has been moved.
*
* @param fileEditorInput the input of an text editor
* @param path the path of the new location of the file
*/
protected void handleElementMoved(IEditorInput fileEditorInput, IPath path)
{
IWorkspace workspace = ResourcesPlugin.getWorkspace();
IFile newFile = workspace.getRoot().getFile(path);
fireElementMoved(fileEditorInput, createNewEditorInput(newFile));
}
/**
* Sends out the notification that the file serving as document input has been deleted.
*
* @param fileEditorInput the input of an text editor
*/
protected void handleElementDeleted(IEditorInput fileEditorInput)
{
fireElementDeleted(fileEditorInput);
}
protected abstract class SafeChange implements Runnable {
private IEditorInput editorInput;
public SafeChange(IEditorInput input)
{
editorInput = input;
}
protected abstract void execute(IEditorInput input) throws Exception;
@Override
public void run()
{
if (getElementInfo(editorInput) == null) {
fireElementStateChangeFailed(editorInput);
return;
}
try {
execute(editorInput);
} catch (Exception e) {
fireElementStateChangeFailed(editorInput);
}
}
}
/**
* Synchronizes the document with external resource changes.
*/
protected class FileSynchronizer implements IResourceChangeListener, IResourceDeltaVisitor {
protected IEditorInput fileEditorInput;
protected boolean isInstalled = false;
/**
* Creates a new file synchronizer. Is not yet installed on a resource.
*
* @param fileEditorInput the editor input to be synchronized
*/
public FileSynchronizer(IEditorInput fileEditorInput)
{
this.fileEditorInput = fileEditorInput;
}
/**
* Returns the file wrapped by the file editor input.
*
* @return the file wrapped by the editor input associated with that synchronizer
*/
protected IFile getFile()
{
IStorage storage = EditorUtils.getStorageFromInput(fileEditorInput);
return storage instanceof IFile ? (IFile)storage : null;
}
/**
* Installs the synchronizer on the input's file.
*/
public void install()
{
getFile().getWorkspace().addResourceChangeListener(this);
isInstalled = true;
}
/**
* Uninstalls the synchronizer from the input's file.
*/
public void uninstall()
{
getFile().getWorkspace().removeResourceChangeListener(this);
isInstalled = false;
}
@Override
public void resourceChanged(IResourceChangeEvent e)
{
IResourceDelta delta = e.getDelta();
try {
if (delta != null && isInstalled) {
delta.accept(this);
}
} catch (CoreException x) {
log.warn("Error handling resourceChanged", x);
}
}
@Override
public boolean visit(IResourceDelta delta) throws CoreException
{
if (delta == null) {
return false;
}
IFile file = getFile();
if (file == null) {
return false;
}
delta = delta.findMember(file.getFullPath());
if (delta == null) {
return false;
}
Runnable runnable = null;
switch (delta.getKind()) {
case IResourceDelta.CHANGED:
FileInfo info = (FileInfo) getElementInfo(fileEditorInput);
if (info == null || !canRefreshFromFile(info)) {
break;
}
boolean isSynchronized = computeModificationStamp(file) == info.modificationStamp;
if ((IResourceDelta.ENCODING & delta.getFlags()) != 0 && isSynchronized) {
runnable = new SafeChange(fileEditorInput) {
@Override
protected void execute(IEditorInput input) throws Exception
{
handleElementContentChanged(input);
}
};
}
if (runnable == null && (IResourceDelta.CONTENT & delta.getFlags()) != 0 && !isSynchronized) {
runnable = new SafeChange(fileEditorInput) {
@Override
protected void execute(IEditorInput input) throws Exception
{
handleElementContentChanged(input);
}
};
}
break;
case IResourceDelta.REMOVED:
if ((IResourceDelta.MOVED_TO & delta.getFlags()) != 0) {
final IPath path = delta.getMovedToPath();
runnable = new SafeChange(fileEditorInput) {
@Override
protected void execute(IEditorInput input) throws Exception
{
handleElementMoved(input, path);
}
};
} else {
info = (FileInfo) getElementInfo(fileEditorInput);
if (info != null && canRefreshFromFile(info)) {
runnable = new SafeChange(fileEditorInput) {
@Override
protected void execute(IEditorInput input) throws Exception
{
handleElementDeleted(input);
}
};
}
}
break;
}
if (runnable != null) {
update(runnable);
}
return false;
}
private boolean canRefreshFromFile(FileInfo info)
{
//return !info.fCanBeSaved;
return true;
}
/**
* Posts the update code "behind" the running operation.
*
* @param runnable the update code
*/
protected void update(Runnable runnable)
{
if (runnable instanceof SafeChange) {
fireElementStateChanging(fileEditorInput);
}
IWorkbench workbench = PlatformUI.getWorkbench();
IWorkbenchWindow[] windows = workbench.getWorkbenchWindows();
if (windows != null && windows.length > 0) {
DBeaverUI.asyncExec(runnable);
} else {
runnable.run();
}
}
}
/**
* Bundle of all required information to allow files as underlying document resources.
*/
protected class FileInfo extends ElementInfo {
/**
* The file synchronizer.
*/
public FileSynchronizer fileSynchronizer;
/**
* The time stamp at which this provider changed the file.
*/
public long modificationStamp = IResource.NULL_STAMP;
public FileInfo(IDocument document, IAnnotationModel model, FileSynchronizer fileSynchronizer)
{
super(document, model);
this.fileSynchronizer = fileSynchronizer;
}
}
}