/** * 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 - * - asynchronous file upload */ package tern.resources; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.Writer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.w3c.dom.Document; import org.w3c.dom.Node; import com.eclipsesource.json.Json; import com.eclipsesource.json.JsonArray; import com.eclipsesource.json.JsonObject; import com.eclipsesource.json.JsonValue; import com.eclipsesource.json.ParseException; import com.eclipsesource.json.WriterConfig; import tern.EcmaVersion; import tern.ITernFile; import tern.ITernFileSynchronizer; import tern.ITernProject; import tern.TernException; import tern.internal.resources.InternalTernResourcesManager; import tern.repository.ITernRepository; import tern.scriptpath.ITernScriptPath; import tern.scriptpath.impl.dom.DOMElementsScriptPath; import tern.server.ITernDef; import tern.server.ITernPlugin; import tern.server.ITernServer; import tern.server.TernDef; import tern.server.protocol.ITernResultsCollector; import tern.server.protocol.JsonHelper; import tern.server.protocol.TernDoc; import tern.server.protocol.TernQuery; import tern.server.protocol.lint.ITernLintCollector; import tern.server.protocol.push.IMessageHandler; import tern.utils.IOUtils; /** * Tern project configuration. * * <pre> * A .tern-project file is a JSON file in a format like this: * * { * "libs": [ * "browser", * "jquery" * ], * "loadEagerly": [ * "importantfile.js" * ], * "plugins": { * "requirejs": { * "baseURL": "./", * "paths": {} * } * } * } * </pre> * * @see http://ternjs.net/doc/manual.html#configuration */ public class TernProject extends JsonObject implements ITernProject { private static final long serialVersionUID = 1L; private static final String ECMA_VERSION_FIELD_NAME = "ecmaVersion"; //$NON-NLS-1$ private static final String PLUGINS_FIELD_NAME = "plugins"; //$NON-NLS-1$ private static final String LIBS_FIELD_NAME = "libs"; //$NON-NLS-1$ private static final String LOAD_EAGERLY_FIELD_NAME = "loadEagerly"; //$NON-NLS-1$ private final File projectDir; private File ternProjectFile; private ITernRepository repository; protected final Object serverLock = new Object(); private ITernPlugin[] linters; /** * tern file synchronizer. */ private ITernFileSynchronizer fileSynchronizer; private JsonObject lastTernProject; private Object libLock = new Object(); private List<ITernPlugin> lastLinters; private final Map<String, List<IMessageHandler>> messageListeners; /** * Tern project constructor. * * @param projectDir * the project base dir. */ public TernProject(File projectDir) { this.projectDir = projectDir; this.ternProjectFile = new File(projectDir, TERN_PROJECT_FILE); this.fileSynchronizer = InternalTernResourcesManager.getInstance().createTernFileSynchronizer(this); this.messageListeners = new HashMap<String, List<IMessageHandler>>(); } @Override public String getName() { return projectDir.getName(); } /** * Returns the project base dir. * * @return the project base dir. */ @Override public File getProjectDir() { return projectDir; } @Override public File getTernProjectFile() { return ternProjectFile; } @Override public void setEcmaVersion(EcmaVersion ecmaVersion) { super.set(ECMA_VERSION_FIELD_NAME, ecmaVersion.getVersion()); } @Override public EcmaVersion getEcmaVersion() { int version = super.getInt(ECMA_VERSION_FIELD_NAME, -1); return EcmaVersion.get(version); } /** * Returns true if lib or plugins exists and false otheriwse. * * @return true if lib or plugins exists and false otheriwse. */ public boolean hasModules() { return hasLibs() || hasPlugins(); } /** * Add JSON Type Definition. * * @param lib * the JSON Type Definition. */ @Override public void addLib(ITernDef lib) { addLib(lib.getName()); } /** * Add JSON Type Definition. * * @param lib * the JSON Type Definition. */ @Override public void addLib(String lib) { synchronized (libLock) { if (!hasLib(lib)) { getLibs().add(lib); } } } public boolean hasLib(TernDef lib) { return hasLib(lib.getName()); } /** * Returns true if the given lib exists and false otherwise. * * @param lib * @return true if the given lib exists and false otherwise. */ @Override public boolean hasLib(String lib) { synchronized (libLock) { JsonArray libs = getLibs(); if (libs != null) { for (JsonValue l : libs) { if (l.isString() && l.asString().equals(lib)) return true; } } return false; } } /** * Returns true if lib exist and false otherwise. * * @return */ public boolean hasLibs() { return super.get(LIBS_FIELD_NAME) != null; } /** * Returns true if the given lib exists and false otherwise. * * @param lib * @return true if the given lib exists and false otherwise. */ @Override public boolean hasLib(ITernDef lib) { return hasLib(lib.getName()); } /** * Return the JSON Type Definitions of the tern project. * * @return the JSON Type Definitions of the tern project. */ @Override public JsonArray getLibs() { synchronized (libLock) { JsonArray libs = (JsonArray) super.get(LIBS_FIELD_NAME); if (libs == null) { libs = new JsonArray(); add(LIBS_FIELD_NAME, libs); } return libs; } } /** * Clear JSON Type Definitions. */ @Override public void clearLibs() { synchronized (libLock) { remove(LIBS_FIELD_NAME); } } /** * Returns true if plugins exist and false otherwise. * * @return */ public boolean hasPlugins() { return super.get(PLUGINS_FIELD_NAME) != null; } /** * Add Tern plugin. * * @param plugin * the tern plugin to add. * @return true if plugin to add, replace an existing plugin and false * otherwise. */ @Override public void addPlugin(ITernPlugin plugin) { addPlugin(plugin, null); } /** * Add Tern plugin with options. * * @param plugin * the tern plugin to add. * @param options * plugin options. */ @Override public void addPlugin(ITernPlugin plugin, JsonValue options) { JsonObject plugins = getPlugins(); if (options == null) options = new JsonObject(); if (!hasPlugin(plugin)) { plugins.add(plugin.getName(), options); } else { if (!JsonHelper.isSameJson(plugins.get(plugin.getName()), options)) { plugins.set(plugin.getName(), options); } } } /** * Returns true if the given plugin exists and false otherwise. * * @param plugin * @return true if the given plugin exists and false otherwise. */ @Override public boolean hasPlugin(ITernPlugin plugin) { return hasPlugin(plugin.getName()); } /** * Returns true if the given plugin exists and false otherwise. * * @param plugin * @return true if the given plugin exists and false otherwise. */ @Override public boolean hasPlugin(String plugin) { return getPlugin(this, plugin) != null; } private static JsonValue getPlugin(JsonObject json, String plugin) { JsonObject plugins = (JsonObject) json.get(PLUGINS_FIELD_NAME); return plugins == null ? null : plugins.get(plugin); } /** * Return the JSON plugins of the tern project. * * @return the JSON plugins of the tern project. */ @Override public JsonObject getPlugins() { JsonObject plugins = (JsonObject) super.get(PLUGINS_FIELD_NAME); if (plugins == null) { plugins = new JsonObject(); add(PLUGINS_FIELD_NAME, plugins); } return plugins; } /** * Clear plugins. */ @Override public void clearPlugins() { remove(PLUGINS_FIELD_NAME); this.linters = null; } @Override public ITernPlugin[] getLinters() { if (linters == null) { Collection<ITernPlugin> plugins = new ArrayList<ITernPlugin>(); collectLinters(plugins); linters = plugins.toArray(ITernPlugin.EMPTY_PLUGIN); } return linters; } protected void collectLinters(Collection<ITernPlugin> plugins) { // dynamic linter coming from repository ITernRepository repository = getRepository(); if (repository != null) { addLinter(plugins, repository.getLinters()); } } private void addLinter(Collection<ITernPlugin> plugins, ITernPlugin[] knownLintPlugins) { ITernPlugin knownLintPlugin; for (int i = 0; i < knownLintPlugins.length; i++) { knownLintPlugin = knownLintPlugins[i]; if (hasPlugin(knownLintPlugin) && !plugins.contains(knownLintPlugin)) { plugins.add(knownLintPlugin); } } } public void addLoadEagerlyPattern(String pattern) { JsonArray patterns = (JsonArray) super.get(LOAD_EAGERLY_FIELD_NAME); if (patterns == null) { patterns = new JsonArray(); add(LOAD_EAGERLY_FIELD_NAME, patterns); } patterns.add(pattern); } /** * Save the tern project in the file .tern-project of the project base dir. * * @throws IOException */ @Override public final void save() throws IOException { try { doSave(); } finally { // reset(); } } /** * Save the tern project in the file .tern-project of the project base dir. * * @throws IOException */ protected void doSave() throws IOException { if (isDirty()) { getProjectDir().mkdirs(); Writer writer = null; try { writer = new FileWriter(ternProjectFile); super.writeTo(writer, WriterConfig.PRETTY_PRINT); } finally { if (writer != null) { IOUtils.closeQuietly(writer); } } reset(); } } /** * Load the tern project from the .tern-project of the project base dir. * * @throws IOException */ public final void load() throws IOException { try { Collection<ITernPlugin> lastLinters = getLastLinters(); JsonObject lastTernProject = this.lastTernProject; doLoad(); if (lastLinters != null) { ITernPlugin[] newLinters = getLinters(); if (isLintersChanged(Arrays.asList(newLinters), this, lastLinters, lastTernProject)) { onLintersChanged(); } } } finally { reset(); this.lastLinters = new ArrayList<ITernPlugin>(); collectLinters(lastLinters); } } /** * Listener lanched when linter is added or removed from the .tern-project. */ protected void onLintersChanged() { } /** * Returns true if linters has changed and false otherwise. * * @param oldLinters * @param newLinters * @param lastTernProject * @return true if linters has changed and false otherwise. */ private boolean isLintersChanged(Collection<ITernPlugin> newLinters, JsonObject newTernProject, Collection<ITernPlugin> lastLinters, JsonObject lastTernProject) { if (!lastLinters.equals(newLinters)) { // some linter was added/removed return true; } if (lastTernProject == null) { return false; } // check if linter options has changed. JsonValue lastOptions = null; JsonValue newOptions = null; for (ITernPlugin linter : lastLinters) { lastOptions = getPlugin(lastTernProject, linter.getName()); newOptions = getPlugin(newTernProject, linter.getName()); if (!JsonHelper.isSameJson(lastOptions, newOptions)) { return true; } } return false; } protected void reset() { this.lastTernProject = Json.parse(toString()).asObject(); this.linters = null; } public List<ITernPlugin> getLastLinters() { return lastLinters; } /** * Load the tern project from the .tern-project of the project base dir. * * @throws IOException */ protected void doLoad() throws IOException { if (ternProjectFile.exists()) { try { JsonHelper.readFrom(new FileReader(ternProjectFile), this); } catch (ParseException e) { throw new IOException(e); } } else { createEmptyTernProjectFile(); } reset(); } /** * Create an empty a .tern-project file. * * @throws IOException */ protected void createEmptyTernProjectFile() throws IOException { save(); } @Override public void handleException(Throwable t) { t.printStackTrace(); } /** * Returns file cache manager. * * @return */ @Override public ITernFileSynchronizer getFileSynchronizer() { return fileSynchronizer; } @Override public ITernServer getTernServer() { return null; } @Override public ITernFile getFile(String name) { return InternalTernResourcesManager.getInstance().getTernFile(this, name); } @Override public ITernFile getFile(Object fileObject) { return InternalTernResourcesManager.getInstance().getTernFile(fileObject); } @Override public List<ITernScriptPath> getScriptPaths() { return Collections.emptyList(); } @Override public Object getAdapter(@SuppressWarnings("rawtypes") Class adapterClass) { if (adapterClass == File.class) { return getProjectDir(); } return null; } protected void synchronize(TernDoc doc, JsonArray names, ITernScriptPath scriptPath, Node domNode, ITernFile file) { ITernFileSynchronizer synchronizer = getFileSynchronizer(); synchronizer.ensureSynchronized(); if (file != null) { if (doc.getQuery() != null) { doc.getQuery().setFile(file.getFullName(this)); } if (domNode != null) { DOMElementsScriptPath domPath = createDOMElementsScriptPath(domNode, file); synchronizer.synchronizeScriptPath(domPath, file.getFullName(this)); } else { try { synchronizer.synchronizeFile(doc, file); } catch (IOException e) { handleException(e); } } } if (names != null) { synchronizer.fillSyncedFileNames(names, scriptPath); } } protected DOMElementsScriptPath createDOMElementsScriptPath(Node domNode, ITernFile file) { final Document doc = domNode.getOwnerDocument(); return new DOMElementsScriptPath(this, file, null) { @Override protected Document getDocument() { return doc; } }; } @Override public void request(TernQuery query, ITernFile file, ITernResultsCollector collector) throws IOException, TernException { request(query, null, null, null, file, collector); } @Override public void request(TernQuery query, JsonArray names, ITernScriptPath scriptPath, Node domNode, ITernFile file, ITernResultsCollector collector) throws IOException, TernException { TernDoc doc = new TernDoc(query); synchronize(doc, names, scriptPath, domNode, file); ITernServer server = getTernServer(); server.request(doc, collector); } @Override public void request(TernQuery query, ITernFile file, boolean synch, ITernLintCollector collector) throws IOException, TernException { TernDoc doc = new TernDoc(query); if (synch) { synchronize(doc, null, null, null, file); } else if (file != null) { if (doc.getQuery() != null) { doc.getQuery().setFile(file.getFullName(this)); } } ITernServer server = getTernServer(); server.request(doc, collector); } @Override public void request(TernQuery query, ITernLintCollector collector) throws IOException, TernException { request(query, null, true, collector); } @Override public ITernRepository getRepository() { return repository; } @Override public void setRepository(ITernRepository repository) { this.repository = repository; } public boolean isDirty() { return !JsonHelper.isSameJson(this, lastTernProject); } @Override public void on(String type, IMessageHandler handler) { synchronized (messageListeners) { List<IMessageHandler> handlers = messageListeners.get(type); if (handlers == null) { handlers = new ArrayList<IMessageHandler>(); messageListeners.put(type, handlers); } if (!handlers.contains(handler)) { handlers.add(handler); } } synchronized (serverLock) { ITernServer ternServer = getTernServer(); if (ternServer != null) { ternServer.on(type, handler); } } } @Override public void off(String type, IMessageHandler handler) { synchronized (messageListeners) { List<IMessageHandler> handlers = messageListeners.get(type); if (handlers != null) { handlers.remove(handler); } } synchronized (serverLock) { ITernServer ternServer = getTernServer(); if (ternServer != null) { ternServer.off(type, handler); } } } protected void copyMessageListeners() { synchronized (serverLock) { ITernServer ternServer = getTernServer(); if (ternServer != null) { Set<Entry<String, List<IMessageHandler>>> entries = messageListeners.entrySet(); String type = null; List<IMessageHandler> handlers; for (Entry<String, List<IMessageHandler>> entry : entries) { type = entry.getKey(); handlers = entry.getValue(); for (IMessageHandler handler : handlers) { ternServer.on(type, handler); } } } } } }