/*******************************************************************************
* Copyright (C) 2011, 2012, 2015 Dariusz Luksza <dariusz@luksza.org> and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial implementation of some methods
* Thomas Wolf <thomas.wolf@paranor.ch> - Bugs 474981, 481682
*******************************************************************************/
package org.eclipse.egit.ui.internal.synchronize.compare;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.Collections;
import org.eclipse.compare.ISharedDocumentAdapter;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.resources.WorkspaceJob;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.egit.core.internal.indexdiff.IndexDiffCacheEntry;
import org.eclipse.egit.core.internal.util.ResourceUtil;
import org.eclipse.egit.core.project.RepositoryMapping;
import org.eclipse.egit.ui.Activator;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.events.IndexChangedEvent;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.util.FileUtils;
import org.eclipse.team.internal.ui.synchronize.EditableSharedDocumentAdapter;
import org.eclipse.team.internal.ui.synchronize.LocalResourceTypedElement;
/**
* Specialized resource node for non-workspace files
*/
@SuppressWarnings("restriction")
public class LocalNonWorkspaceTypedElement extends LocalResourceTypedElement {
@NonNull
private final IPath path;
@NonNull
private final Repository repository;
private boolean exists;
private boolean fDirty;
private long timestamp;
private boolean useSharedDocument = true;
private EditableSharedDocumentAdapter sharedDocumentAdapter;
private EditableSharedDocumentAdapter.ISharedDocumentAdapterListener sharedDocumentListener;
private static final IWorkspaceRoot ROOT = ResourcesPlugin.getWorkspace().getRoot();
/**
* @param repository
* the file belongs to
* @param path
* absolute path to non-workspace file
*/
public LocalNonWorkspaceTypedElement(@NonNull Repository repository,
@NonNull IPath path) {
super(ROOT.getFile(path));
this.path = path;
this.repository = repository;
File file = path.toFile();
exists = file.exists() || Files.isSymbolicLink(file.toPath());
if (exists) {
timestamp = file.lastModified();
}
}
@Override
public InputStream getContents() throws CoreException {
if (exists) {
try {
File file = path.toFile();
timestamp = file.lastModified();
if (Files.isSymbolicLink(file.toPath())) {
String symLink = FileUtils.readSymLink(file);
return new ByteArrayInputStream(Constants.encode(symLink));
}
if (file.isDirectory()) {
// submodule
Repository sub = ResourceUtil.getRepository(path);
if (sub != null && sub != repository) {
RevCommit headCommit = Activator.getDefault()
.getRepositoryUtil().parseHeadCommit(sub);
if (headCommit == null) {
return null;
}
return new ByteArrayInputStream(Constants
.encode(headCommit.name()));
}
}
return new FileInputStream(file);
} catch (IOException | UnsupportedOperationException e) {
Activator.error(e.getMessage(), e);
}
}
return null;
}
/** {@inheritDoc} */
@Override
public boolean isEditable() {
IResource resource = getResource();
return resource.getType() == IResource.FILE && exists;
}
@Override
public long getModificationDate() {
return timestamp;
}
@Override
public boolean isSynchronized() {
return path.toFile().lastModified() == timestamp;
}
/** {@inheritDoc} */
@Override
public void update() {
exists = path.toFile().exists();
}
/** {@inheritDoc} */
@Override
public boolean exists() {
return exists;
}
/** {@inheritDoc} */
@Override
public boolean isSharedDocumentsEnable() {
return useSharedDocument && getResource().getType() == IResource.FILE && exists;
}
/** {@inheritDoc} */
@Override
public void enableSharedDocument(boolean enablement) {
this.useSharedDocument = enablement;
}
/** {@inheritDoc} */
@Override
public void setContent(byte[] contents) {
fDirty = true;
super.setContent(contents);
}
private void refreshTimestamp() {
timestamp = path.toFile().lastModified();
}
/** {@inheritDoc} */
@Override
public void commit(IProgressMonitor monitor) throws CoreException {
if (isDirty()) {
if (isConnected()) {
super.commit(monitor);
} else {
File file = path.toFile();
try {
java.nio.file.Path fp = file.toPath();
if (Files.isSymbolicLink(fp)) {
String sp = new String(getContent(), Constants.CHARSET)
.trim();
if (sp.indexOf('\n') > 0) {
sp = sp.substring(0, sp.indexOf('\n')).trim();
}
if (!sp.isEmpty()) {
boolean wasBrokenLink = !file.exists();
java.nio.file.Path link = FileUtils
.createSymLink(file, sp);
// If link state changes, Eclipse can't realize this
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=290318
updateLinkResource(wasBrokenLink, link);
}
} else {
if (!file.exists()) {
FileUtils.createNewFile(file);
}
try (FileOutputStream out = new FileOutputStream(
file)) {
out.write(getContent());
}
}
fDirty = false;
} catch (IOException e) {
throw new CoreException(
new Status(
IStatus.ERROR,
Activator.getPluginId(),
UIText.LocalNonWorkspaceTypedElement_errorWritingContents,
e));
} finally {
fireChanges();
}
}
refreshTimestamp();
}
}
private void updateLinkResource(boolean wasBroken,
java.nio.file.Path link) {
boolean brokenNow = !Files.exists(link);
if (brokenNow == wasBroken) {
// If the state doesn't change, we don't care, either Eclipse
// doesn's see broken link and we can't do anything or it is not
// broken and Eclipse handles the change
return;
}
// refresh the parent if either the link was broken before or broken
// just now
IPath parentPath = path.removeLastSegments(1);
@SuppressWarnings("null")
final IContainer parent = ResourceUtil
.getContainerForLocation(parentPath, true);
if (parent != null) {
WorkspaceJob job = new WorkspaceJob("Refreshing " + parentPath) { //$NON-NLS-1$
@Override
public IStatus runInWorkspace(IProgressMonitor m)
throws CoreException {
parent.refreshLocal(IResource.DEPTH_ONE, m);
return Status.OK_STATUS;
}
};
job.setSystem(true);
job.schedule();
}
}
private void fireChanges() {
fireContentChanged();
// external file change must be reported explicitly, see bug 481682
Repository myRepository = repository;
boolean updated = false;
if (!myRepository.isBare()) {
updated = refreshRepositoryState(myRepository);
}
if (!updated) {
RepositoryMapping mapping = RepositoryMapping.getMapping(path);
if (mapping != null) {
mapping.getRepository().fireEvent(new IndexChangedEvent());
}
}
}
private boolean refreshRepositoryState(@NonNull Repository repo) {
IPath repositoryRoot = new Path(repo.getWorkTree().getPath());
IPath relativePath = path.makeRelativeTo(repositoryRoot);
IndexDiffCacheEntry indexDiffCacheForRepository = org.eclipse.egit.core.Activator
.getDefault().getIndexDiffCache().getIndexDiffCacheEntry(repo);
if (indexDiffCacheForRepository != null) {
indexDiffCacheForRepository.refreshFiles(
Collections.singleton(relativePath.toString()));
return true;
}
return false;
}
/** {@inheritDoc} */
@Override
public synchronized boolean isDirty() {
return fDirty || (sharedDocumentAdapter != null && sharedDocumentAdapter.hasBufferedContents());
}
@Override
public Object getAdapter(Class adapter) {
if (adapter == ISharedDocumentAdapter.class) {
if (isSharedDocumentsEnable())
return getSharedDocumentAdapter();
else
return null;
}
return Platform.getAdapterManager().getAdapter(this, adapter);
}
@Override
public void setSharedDocumentListener(
EditableSharedDocumentAdapter.ISharedDocumentAdapterListener sharedDocumentListener) {
this.sharedDocumentListener = sharedDocumentListener;
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + path.hashCode();
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!super.equals(obj))
return false;
if (getClass() != obj.getClass())
return false;
LocalNonWorkspaceTypedElement other = (LocalNonWorkspaceTypedElement) obj;
if (!path.equals(other.path))
return false;
return true;
}
/*
* Returned the shared document adapter for this element. If one does not exist
* yet, it will be created.
*/
private synchronized ISharedDocumentAdapter getSharedDocumentAdapter() {
if (sharedDocumentAdapter == null)
sharedDocumentAdapter = new EditableSharedDocumentAdapter(new EditableSharedDocumentAdapter.ISharedDocumentAdapterListener() {
@Override
public void handleDocumentConnected() {
refreshTimestamp();
if (sharedDocumentListener != null)
sharedDocumentListener.handleDocumentConnected();
}
@Override
public void handleDocumentFlushed() {
fireContentChanged();
if (sharedDocumentListener != null)
sharedDocumentListener.handleDocumentFlushed();
}
@Override
public void handleDocumentDeleted() {
LocalNonWorkspaceTypedElement.this.update();
if (sharedDocumentListener != null)
sharedDocumentListener.handleDocumentDeleted();
}
@Override
public void handleDocumentSaved() {
refreshTimestamp();
if (sharedDocumentListener != null)
sharedDocumentListener.handleDocumentSaved();
}
@Override
public void handleDocumentDisconnected() {
if (sharedDocumentListener != null)
sharedDocumentListener.handleDocumentDisconnected();
}
});
return sharedDocumentAdapter;
}
}