/**
* Copyright (c) 2013-2016 Angelo ZERR and Genuitec LLC.
* 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:
* Angelo Zerr <angelo.zerr@gmail.com> - initial API and implementation
* Piotr Tomiak <piotr@genuitec.com> - refactoring of file management API
*/
package tern.eclipse.ide.internal.core.resources;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceVisitor;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.QualifiedName;
import com.eclipsesource.json.JsonArray;
import com.eclipsesource.json.JsonObject;
import com.eclipsesource.json.WriterConfig;
import tern.ITernFile;
import tern.ITernProject;
import tern.TernException;
import tern.TernResourcesManager;
import tern.eclipse.ide.core.IIDETernProject;
import tern.eclipse.ide.core.IIDETernScriptPathReporter;
import tern.eclipse.ide.core.IScopeContext;
import tern.eclipse.ide.core.ITernConsoleConnector;
import tern.eclipse.ide.core.ITernProjectLifecycleListener.LifecycleEventType;
import tern.eclipse.ide.core.ITernServerPreferencesListener;
import tern.eclipse.ide.core.ITernServerType;
import tern.eclipse.ide.core.IWorkingCopy;
import tern.eclipse.ide.core.ScopeContext;
import tern.eclipse.ide.core.TernCorePlugin;
import tern.eclipse.ide.core.utils.FileUtils;
import tern.eclipse.ide.internal.core.TernConsoleConnectorManager;
import tern.eclipse.ide.internal.core.TernNatureAdaptersManager;
import tern.eclipse.ide.internal.core.TernProjectLifecycleManager;
import tern.eclipse.ide.internal.core.TernRepositoryManager;
import tern.eclipse.ide.internal.core.Trace;
import tern.eclipse.ide.internal.core.WorkingCopy;
import tern.eclipse.ide.internal.core.preferences.TernCorePreferencesSupport;
import tern.eclipse.ide.internal.core.scriptpath.EclipseProjectScriptPath;
import tern.eclipse.ide.internal.core.scriptpath.FolderScriptPath;
import tern.eclipse.ide.internal.core.scriptpath.IIDETernScriptPath;
import tern.eclipse.ide.internal.core.scriptpath.TernScriptPathComparator;
import tern.repository.ITernRepository;
import tern.resources.TernFileSynchronizer;
import tern.resources.TernProject;
import tern.scriptpath.ITernScriptPath;
import tern.scriptpath.ITernScriptPath.ScriptPathsType;
import tern.scriptpath.ITernScriptPathContainer;
import tern.scriptpath.impl.JSFileScriptPath;
import tern.scriptpath.impl.dom.DOMElementsScriptPath;
import tern.server.ITernModule;
import tern.server.ITernServer;
import tern.server.ITernServerListener;
import tern.server.TernServerAdapter;
import tern.server.protocol.JsonHelper;
import tern.utils.IOUtils;
import tern.utils.StringUtils;
import tern.utils.TernModuleHelper;
/**
* Eclipse IDE Tern project.
*
*/
public class IDETernProject extends TernProject implements IIDETernProject, ITernServerPreferencesListener {
private static final QualifiedName TERN_PROJECT = new QualifiedName(TernCorePlugin.PLUGIN_ID + ".sessionprops", //$NON-NLS-1$
"TernProject"); //$NON-NLS-1$
private static final String PATH_JSON_FIELD = "path"; //$NON-NLS-1$
private static final String TYPE_JSON_FIELD = "type"; //$NON-NLS-1$
private static final String INCLUSION_PATTERNS_JSON_FIELD = "inclusionPatterns"; //$NON-NLS-1$
private static final String EXCLUSION_PATTERNS_JSON_FIELD = "exclusionPatterns"; //$NON-NLS-1$
private static final String SCRIPT_PATHS_JSON_FIELD = "scriptPaths"; //$NON-NLS-1$
private static final String IDE_JSON_FIELD = "ide"; //$NON-NLS-1$
private static final long serialVersionUID = 1L;
private final IProject project;
private ITernServer ternServer;
private final List<ITernScriptPath> scriptPaths;
private List<ITernScriptPath> sortedScriptPaths;
private final Map<String, Object> data;
private final List<ITernServerListener> listeners;
private final IWorkingCopy workingCopy;
IDETernProject(IProject project) throws CoreException {
super(project.getLocation().toFile());
this.project = project;
this.scriptPaths = new ArrayList<ITernScriptPath>();
this.sortedScriptPaths = Collections.emptyList();
this.data = new HashMap<String, Object>();
this.listeners = new ArrayList<ITernServerListener>();
this.workingCopy = new WorkingCopy(this);
TernCorePlugin.getTernServerTypeManager().addServerPreferencesListener(this);
project.setSessionProperty(TERN_PROJECT, this);
}
/**
* Returns the Eclispe project.
*
* @return
*/
@Override
public IProject getProject() {
return project;
}
@Override
public String getName() {
return project.getName();
}
/**
* Returns the linked instance of tern server.
*
* @return
*/
@Override
public ITernServer getTernServer() {
synchronized (serverLock) {
if (isServerDisposed()) {
try {
ITernServerType type = TernCorePreferencesSupport.getInstance().getServerType();
this.ternServer = type.createServer(this);
this.ternServer.setLoadingLocalPlugins(
TernCorePreferencesSupport.getInstance().isLoadingLocalPlugins(project));
this.ternServer.addServerListener(new TernServerAdapter() {
@Override
public void onStop(ITernServer server) {
getFileSynchronizer().cleanIndexedFiles();
}
});
if (!TernCorePreferencesSupport.getInstance().isDisableAsynchronousReques(project)) {
this.ternServer.setRequestProcessor(new IDETernServerAsyncReqProcessor(ternServer));
}
copyListeners();
copyMessageListeners();
configureConsole();
} catch (Exception e) {
// should be improved?
Trace.trace(Trace.SEVERE, "Error while creating tern server", e);
}
}
return ternServer;
}
}
public boolean isServerDisposed() {
synchronized (serverLock) {
return ternServer == null || ternServer.isDisposed();
}
}
/**
* Return true if the given project have tern nature
* "tern.eclipse.ide.core.ternnature" and false otherwise.
*
* @param project
* Eclipse project.
* @return true if the given project have tern nature
* "tern.eclipse.ide.core.ternnature" and false otherwise.
*/
public static boolean hasTernNature(IProject project) {
return TernNatureAdaptersManager.getManager().hasTernNature(project);
}
@Override
protected void doLoad() throws IOException {
try {
disposeServer();
TernProjectLifecycleManager.getManager().fireTernProjectLifeCycleListenerChanged(this,
LifecycleEventType.onLoadBefore);
super.doLoad();
// Load IDE informations of the tern project.
loadIDEInfos();
// the tern project is loaded on the first time, load default
// modules and save .tern-project.
initAdaptedNaturesInfos();
} finally {
TernProjectLifecycleManager.getManager().fireTernProjectLifeCycleListenerChanged(this,
LifecycleEventType.onLoadAfter);
}
}
@Override
protected void onLintersChanged() {
TernProjectLifecycleManager.getManager().fireTernProjectLifeCycleListenerChanged(this,
LifecycleEventType.onLintersChanged);
}
/**
* Load IDE informations from the JSON .tern-project file.
*/
private void loadIDEInfos() {
// Load script paths
this.scriptPaths.clear();
JsonObject ide = (JsonObject) super.get(IDE_JSON_FIELD);
if (ide != null) {
// There is ide information.
JsonArray jsonScripts = (JsonArray) ide.get(SCRIPT_PATHS_JSON_FIELD);
if (jsonScripts != null) {
// There is scriptPaths defined.
JsonObject jsonScript = null;
String type = null;
String path = null;
// Loop for each script path.
for (Object object : jsonScripts) {
jsonScript = (JsonObject) object;
type = JsonHelper.getString(jsonScript, TYPE_JSON_FIELD);
path = JsonHelper.getString(jsonScript, PATH_JSON_FIELD);
if (type != null && path != null) {
ScriptPathsType pathType = ScriptPathsType.getType(type);
if (pathType == null) {
pathType = ScriptPathsType.FILE;
}
if (pathType != null) {
String[] inclusionPatterns = getPatterns(
JsonHelper.getString(jsonScript, INCLUSION_PATTERNS_JSON_FIELD));
String[] exclusionPatterns = getPatterns(
JsonHelper.getString(jsonScript, EXCLUSION_PATTERNS_JSON_FIELD));
// script path type exists.
IResource resource = getResource(path, pathType);
if (resource != null && resource.exists()) {
// the script path exists, add it.
this.scriptPaths.add(
createScriptPath(resource, pathType, inclusionPatterns, exclusionPatterns));
}
}
}
}
}
}
resetSortScriptPaths();
}
private void resetSortScriptPaths() {
this.sortedScriptPaths = null;
}
private String[] getPatterns(String patterns) {
if (StringUtils.isEmpty(patterns)) {
return null;
}
return patterns.split(",");
}
/*
* Configures Tern Modules (Libraries and Plugins) that are default for Tern
* Nature Adapters active on a project
*/
private void initAdaptedNaturesInfos() {
try {
TernNatureAdaptersManager.getManager().addDefaultModules(this);
} catch (CoreException e) {
Trace.trace(Trace.SEVERE, "Error while configuring default tern project modules", e);
return;
}
try {
save();
} catch (IOException e) {
Trace.trace(Trace.SEVERE, "Error while saving tern project", e);
}
}
/**
* Returns the resource of the given path and type.
*
* @param path
* the path of the script path
* @param pathType
* the type of the script path.
* @return
*/
private IResource getResource(String path, ScriptPathsType pathType) {
switch (pathType) {
case FILE:
return getProject().getFile(path);
case FOLDER:
return getProject().getFolder(path);
case PROJECT:
return ResourcesPlugin.getWorkspace().getRoot().getProject(path);
}
throw new UnsupportedOperationException(
"Cannot retrieve resource from the type=" + pathType + " of the path=" + path);
}
@Override
public IFile getIDEFile(String name) {
ITernFile tf = getFile(name);
if (tf != null) {
return (IFile) tf.getAdapter(IFile.class);
}
return null;
}
@Override
protected void doSave() throws IOException {
try {
TernProjectLifecycleManager.getManager().fireTernProjectLifeCycleListenerChanged(this,
LifecycleEventType.onSaveBefore);
// Store IDE tern project info.
saveIDEInfos();
if (isDirty()) {
// save .tern-project
IFile file = project.getFile(TERN_PROJECT_FILE);
InputStream content = null;
try {
content = IOUtils.toInputStream(super.toString(WriterConfig.PRETTY_PRINT),
file.exists() ? file.getCharset() : StringUtils.UTF_8);
if (!file.exists()) {
file.create(content, IResource.NONE, null);
} else {
file.setContents(content, true, false, null);
}
} catch (CoreException e) {
throw new IOException("Cannot save .tern-project", e);
} finally {
if (content != null) {
IOUtils.closeQuietly(content);
}
}
// .tern-project has changed, dispose the server.
disposeServer();
}
} finally {
TernProjectLifecycleManager.getManager().fireTernProjectLifeCycleListenerChanged(this,
LifecycleEventType.onSaveAfter);
}
}
@Override
public void handleException(Throwable t) {
Trace.trace(Trace.SEVERE, t.getMessage(), t);
}
/**
* Save IDE informations in the JSON file .tern-project.
*/
private void saveIDEInfos() {
// script path
if (scriptPaths.size() > 0) {
JsonObject ide = new JsonObject();
JsonArray jsonScripts = new JsonArray();
// Loop for each script path and save it in the JSON file
// .tern-project.
for (ITernScriptPath scriptPath : scriptPaths) {
if (!scriptPath.isExternal()) {
JsonObject jsonScript = new JsonObject();
jsonScript.add(TYPE_JSON_FIELD, scriptPath.getType().name());
jsonScript.add(PATH_JSON_FIELD, scriptPath.getPath());
if (scriptPath instanceof ITernScriptPathContainer) {
ITernScriptPathContainer container = (ITernScriptPathContainer) scriptPath;
String exclusionPatterns = toString(container.getExclusionPatterns());
if (exclusionPatterns != null) {
jsonScript.add(EXCLUSION_PATTERNS_JSON_FIELD, exclusionPatterns);
}
String inclusionPatterns = toString(container.getInclusionPatterns());
if (inclusionPatterns != null) {
jsonScript.add(INCLUSION_PATTERNS_JSON_FIELD, inclusionPatterns);
}
}
jsonScripts.add(jsonScript);
}
}
ide.add(SCRIPT_PATHS_JSON_FIELD, jsonScripts);
super.set(IDE_JSON_FIELD, ide);
} else {
super.remove(IDE_JSON_FIELD);
}
}
private String toString(String[] patterns) {
if (patterns == null) {
return null;
}
StringBuilder result = new StringBuilder();
for (int i = 0; i < patterns.length; i++) {
if (i > 0) {
result.append(",");
}
result.append(patterns[i]);
}
return result.toString();
}
/**
* Returns the list of script paths.
*
* @return
*/
@Override
public List<ITernScriptPath> getScriptPaths() {
return scriptPaths;
}
/**
* Create the script path instance from the given resource and type.
*
* @param resource
* the root resource.
* @param type
* of the script path.
* @param inclusionPatterns
* include patterns or null
* @param exclusionPatterns
* exclude patterns or null
* @return
*/
@Override
public ITernScriptPath createScriptPath(IResource resource, ScriptPathsType type, String[] inclusionPatterns,
String[] exclusionPatterns) {
return createScriptPath(resource, type, inclusionPatterns, exclusionPatterns, null);
}
private ITernScriptPath createScriptPath(IResource resource, ScriptPathsType type, String[] inclusionPatterns,
String[] exclusionPatterns, String external) {
switch (type) {
case FOLDER:
return new FolderScriptPath(this, (IFolder) resource, inclusionPatterns, exclusionPatterns, external);
case FILE:
ITernFile file = getFile(resource);
if (file == null) {
break;
}
if (TernResourcesManager.isJSFile(file)) {
return new JSFileScriptPath(this, file, external);
}
return new DOMElementsScriptPath(this, file, external);
case PROJECT:
ITernProject project;
try {
project = TernCorePlugin.getTernProject((IProject) resource);
if (project != null) {
return new EclipseProjectScriptPath(project, this, inclusionPatterns, exclusionPatterns, external);
}
} catch (CoreException e) {
Trace.trace(Trace.SEVERE, "Project " + resource.getName() + " is not a Tern project", e);
}
}
throw new UnsupportedOperationException("Cannot create script path for the given type " + type);
}
/**
* Set the new script paths to use.
*
* @param scriptPaths
* @throws IOException
*/
public void setScriptPaths(List<ITernScriptPath> scriptPaths) throws IOException {
this.scriptPaths.clear();
this.scriptPaths.addAll(scriptPaths);
resetSortScriptPaths();
save();
}
@Override
public ITernScriptPath addExternalScriptPath(IResource resource, ScriptPathsType type, String[] inclusionPatterns,
String[] exclusionPatterns, String external) throws IOException {
ITernScriptPath path = createScriptPath(resource, type, inclusionPatterns, exclusionPatterns, external);
scriptPaths.add(path);
resetSortScriptPaths();
return path;
}
@Override
public void removeExternalScriptPaths(String external) {
List<ITernScriptPath> initialScriptPaths = new ArrayList<ITernScriptPath>(scriptPaths);
for (ITernScriptPath scriptPath : initialScriptPaths) {
if (external.equals(scriptPath.getExternalLabel())) {
scriptPaths.remove(scriptPath);
}
}
resetSortScriptPaths();
}
@Override
public Object getAdapter(@SuppressWarnings("rawtypes") Class adapterClass) {
if (adapterClass == IProject.class || adapterClass == IContainer.class || adapterClass == IResource.class) {
return project;
}
return super.getAdapter(adapterClass);
}
@Override
public boolean equals(Object value) {
if (value instanceof IDETernProject) {
return ((IDETernProject) value).getProject().equals(getProject());
}
return super.equals(value);
}
/**
* Returns the script path instance from the given path and null otherwise.
*
* @param path
* of the script path resource.
* @return the script path instance from the given path and null otherwise.
*/
public ITernScriptPath getScriptPath(String path) {
for (ITernScriptPath scriptPath : scriptPaths) {
if (scriptPath.getPath().equals(path)) {
return scriptPath;
}
}
return null;
}
/**
* Returns true if trace of the tern server (JSON request/response) should
* be displayed on the Eclipse console and false otherwise.
*
* @return
*/
public boolean isTraceOnConsole() {
return TernCorePreferencesSupport.getInstance().isTraceOnConsole(project);
}
/**
* Configure console to show/hide JSON request/response of the tern server.
*/
public void configureConsole() {
synchronized (serverLock) {
if (ternServer != null) {
// There is a tern server instance., Retrieve the well connector
// the
// the eclipse console.
ITernConsoleConnector connector = TernConsoleConnectorManager.getManager().getConnector(ternServer);
if (connector != null) {
if (isTraceOnConsole()) {
// connect the tern server to the eclipse console.
connector.connectToConsole(ternServer, this);
} else {
// disconnect the tern server to the eclipse console.
connector.disconnectToConsole(ternServer, this);
}
}
}
}
}
public void disposeServer() {
synchronized (serverLock) {
if (!isServerDisposed()) {
if (ternServer != null) {
// notify uploader that we are going to dispose the server,
// so that it can finish gracefully
((IDETernFileUploader) ((TernFileSynchronizer) getFileSynchronizer()).getTernFileUploader())
.serverToBeDisposed();
ternServer.dispose();
ternServer = null;
}
}
}
}
@SuppressWarnings("unchecked")
public <T> T getData(String key) {
synchronized (data) {
return (T) data.get(key);
}
}
public void setData(String key, Object value) {
synchronized (data) {
data.put(key, value);
}
}
@Override
public void serverPreferencesChanged(IProject project) {
if (project == null || getProject().equals(project)) {
disposeServer();
}
}
// ----------------------- Tern server listeners.
@Override
public void addServerListener(ITernServerListener listener) {
synchronized (listeners) {
if (!listeners.contains(listener)) {
listeners.add(listener);
}
}
copyListeners();
}
@Override
public void removeServerListener(ITernServerListener listener) {
synchronized (listeners) {
listeners.remove(listener);
}
synchronized (serverLock) {
if (ternServer != null) {
this.ternServer.removeServerListener(listener);
}
}
}
private void copyListeners() {
synchronized (serverLock) {
if (ternServer != null) {
for (ITernServerListener listener : listeners) {
this.ternServer.addServerListener(listener);
}
}
}
}
@Override
public List<ITernModule> getProjectModules() {
final List<ITernModule> modules = new ArrayList<ITernModule>();
final ITernRepository projectRepository = getRepository();
final ITernRepository defaultRepository = TernRepositoryManager.getManager().getDefaultRepository();
if (project.isAccessible() && TernCorePreferencesSupport.getInstance().isLoadingLocalPlugins(project)) {
try {
project.accept(new IResourceVisitor() {
@Override
public boolean visit(IResource resource) throws CoreException {
switch (resource.getType()) {
case IResource.PROJECT:
return true;
case IResource.FILE:
ITernModule module = TernModuleHelper.createModule(resource.getName(), projectRepository,
defaultRepository);
if (module != null) {
modules.add(module);
}
return false;
default:
return false;
}
}
});
} catch (CoreException e) {
Trace.trace(Trace.SEVERE, "Error while collecting tern plugin from the project root", e);
}
}
return modules;
}
@Override
public ITernRepository getRepository() {
return TernRepositoryManager.getManager().getRepository(getProject());
}
public void dispose() throws CoreException {
try {
TernProjectLifecycleManager.getManager().fireTernProjectLifeCycleListenerChanged(this,
LifecycleEventType.onDisposeBefore);
disposeServer();
getFileSynchronizer().dispose();
if (project.isAccessible()) {
project.setSessionProperty(TERN_PROJECT, null);
}
} finally {
TernProjectLifecycleManager.getManager().fireTernProjectLifeCycleListenerChanged(this,
LifecycleEventType.onDisposeAfter);
}
}
protected static IDETernProject getTernProject(IProject project) throws CoreException {
if (project.isAccessible()) {
return (IDETernProject) project.getSessionProperty(TERN_PROJECT);
}
return null;
}
@Override
public List<ITernModule> getAllModules() throws TernException {
// Add global tern module from the repository
List<ITernModule> allModules = new ArrayList<ITernModule>(Arrays.asList(getRepository().getModules()));
// Add local tern modules
List<ITernModule> projectModules = getProjectModules();
allModules.addAll(projectModules);
return allModules;
}
@Override
public IWorkingCopy getWorkingCopy(Object caller) throws TernException {
if (workingCopy.isDirty()) {
workingCopy.initialize();
}
workingCopy.call(caller);
return workingCopy;
}
@Override
public boolean isInScope(IResource resource, IScopeContext context) {
if (context == null) {
context = ScopeContext.DEFAULT;
}
// check if the resource is valid
if (!checkValidResource(resource, context)) {
return false;
}
// check if the resource is in scope with script paths.
return checkInScopeResource(resource, context);
}
/**
* Returns true if the resource is valid and false otherwise.
*
* @param resource
* @param context
* @return true if the resource is valid and false otherwise.
*/
private boolean checkValidResource(IResource resource, IScopeContext context) {
if (!FileUtils.isValidResource(resource)) {
return false;
}
IContainer parent = resource.getParent();
while ((parent.getType() & IResource.PROJECT) == 0) {
if (context.isExclude(parent)) {
return false;
}
if (context.isInclude(parent)) {
return true;
}
if (!FileUtils.isValidResource(parent)) {
context.addExclude(parent);
return false;
}
parent = parent.getParent();
}
return true;
}
/**
* Returns true if the given resource is in the scope of tern script path
* and false otherwise.
*
* @param resource
* @param context
* @return true if the given resource is in the scope of tern script path
* and false otherwise.
*/
private boolean checkInScopeResource(IResource resource, IScopeContext context) {
List<ITernScriptPath> scriptPaths = getSortedScriptPaths();
if (scriptPaths.isEmpty()) {
// none script path, we consider that resource is in the scope?
return true;
}
// Loop for each tern script path
int resourceType = resource.getType();
IPath path = resource.getFullPath();
IIDETernScriptPath s = null;
IContainer parent = null;
for (ITernScriptPath scriptPath : scriptPaths) {
if (scriptPath instanceof IIDETernScriptPath) {
// script path is an Eclipse Project or Folder
s = (IIDETernScriptPath) scriptPath;
if (s.isBelongToContainer(path)) {
// the resource path belongs to the Project/folder of the
// tern script path
if (!s.isInScope(path, resourceType)) {
// the tern script exclude or don't include the resource
return false;
} else if (resourceType == IResource.FILE) {
// None exclusion, check if the parent folder exclude
// the file
parent = resource.getParent();
while ((parent.getType() & IResource.PROJECT) == 0) {
if (context.isInclude(parent)) {
return true;
}
if (!s.isInScope(parent.getFullPath(), parent.getType())) {
context.addExclude(parent);
return false;
}
parent = parent.getParent();
}
}
// The parent folder of the resource
context.addInclude(resource.getParent());
return true;
}
}
}
// resource is not inside a scritp path, exclude it.
return false;
}
private List<ITernScriptPath> getSortedScriptPaths() {
if (sortedScriptPaths == null) {
if (!scriptPaths.isEmpty()) {
sortedScriptPaths = new ArrayList<ITernScriptPath>(scriptPaths);
Collections.sort(sortedScriptPaths, TernScriptPathComparator.INSTANCE);
} else {
sortedScriptPaths = Collections.emptyList();
}
}
return sortedScriptPaths;
}
@Override
public IIDETernScriptPathReporter getScriptPathReporter() {
// Uncomment to have trace for include/exclude files.
return null; // SysErrScriptPathReporter.INSTANCE;
}
}