/** * 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: * Piotr Tomiak <piotr@genuitec.com> - initial API and implementation * Angelo Zerr <angelo.zerr@gmail.com> - initial API and implementation */ package tern.resources; import java.io.IOException; import java.text.MessageFormat; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; import tern.ITernFileSynchronizer; import tern.ITernFile; import tern.ITernProject; import tern.TernResourcesManager; import tern.scriptpath.ITernScriptResource; import tern.scriptpath.ITernScriptPath; import tern.server.ITernServer; import tern.server.protocol.TernDoc; import tern.server.protocol.TernFile; import tern.server.protocol.TernFile.FileType; import tern.server.protocol.TernQuery; import tern.server.protocol.completions.TernCompletionsQuery; import tern.server.protocol.definition.TernDefinitionQuery; import tern.server.protocol.lint.TernLintQuery; import tern.server.protocol.type.TernTypeQuery; import tern.utils.StringUtils; import com.eclipsesource.json.JsonArray; import com.eclipsesource.json.JsonValue; /** * Tern file synchronizer is used to maintain a cache with indexed files which * was already parsed by the tern server to avoid parsing files on each tern * request. It is also responsible to keep up-to-date version of those files on * the server. */ public class TernFileSynchronizer implements ITernFileSynchronizer { // wait 200ms for uploader to finish private static final int TIMEOUT = 200; // allow to upload maximum 12MB private static final int MAX_ALLOWED_SIZE = 12 * 1024 * 1024; private final ITernProject project; private final ITernFileUploader uploader; // Access synchronization private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private final ReadLock readLock = lock.readLock(); private final WriteLock writeLock = lock.writeLock(); // Files synchronization private final Map<String, String> sentFiles = new HashMap<String, String>(); private final Set<String> toRefresh = new HashSet<String>(); private final Map<ITernScriptPath, Set<String>> syncedFilesPerPath = new HashMap<ITernScriptPath, Set<String>>(); private ITernServer targetServer; /** * Tern file manager constructor. */ public TernFileSynchronizer(ITernProject project) { this.project = project; this.uploader = createTernFileUploader(); } public ITernFileUploader getTernFileUploader() { return uploader; } protected ITernFileUploader createTernFileUploader() { return new SynchronousTernFileUploader(project); } @Override public ITernProject getProject() { return project; } @Override public void uploadFailed(TernDoc doc) { synchronized (toRefresh) { for (JsonValue val : doc.getFiles()) { if (val instanceof TernFile) { toRefresh.add(((TernFile) val).getName()); } } } } @Override public void refresh(Object file) { ITernFile tf = TernResourcesManager.getTernFile(file); if (tf != null) { synchronized (toRefresh) { toRefresh.add(tf.getFullName(getProject())); } } } @Override public void cleanIndexedFiles() { writeLock.lock(); try { sentFiles.clear(); syncedFilesPerPath.clear(); } finally { writeLock.unlock(); } synchronized (toRefresh) { toRefresh.clear(); } } @Override public void fillSyncedFileNames(JsonArray fileNames, ITernScriptPath path) { readLock.lock(); try { Set<String> files; if (path != null) { files = syncedFilesPerPath.get(path); } else { files = sentFiles.keySet(); } if (files != null) { for (String file : files) { fileNames.add(file); } } } finally { readLock.unlock(); } } protected void sizeExceeded() { System.out .println(MessageFormat .format("Size of scripts on {0} script path exceeds 12MB. Content assist might be incomplete.", getProject().getName())); } @Override public void ensureSynchronized() { TernDoc doc = new TernDoc(); writeLock.lock(); try { if (project.getTernServer() != targetServer) { targetServer = project.getTernServer(); cleanIndexedFiles(); } syncedFilesPerPath.clear(); Set<String> synced = new HashSet<String>(sentFiles.keySet()); Set<String> toRefreshLocal = new HashSet<String>(); synchronized (toRefresh) { toRefreshLocal.addAll(toRefresh); toRefresh.clear(); } synced.removeAll(toRefreshLocal); long totalSize = 0; for (String file : synced) { totalSize += sentFiles.get(file).length(); } for (ITernScriptPath path : getProject().getScriptPaths()) { Set<String> perPath = new HashSet<String>(); syncedFilesPerPath.put(path, perPath); for (ITernScriptResource resource : path.getScriptResources()) { // limit the size of content being sent to the Tern server if (totalSize >= MAX_ALLOWED_SIZE) { sizeExceeded(); break; } ITernFile file = resource.getFile(); if (file == null) { continue; } String name = file.getFullName(getProject()); perPath.add(name); if (!synced.contains(name)) { try { TernFile tf = file.toTernServerFile(getProject()); doc.addFile(tf); synced.add(name); totalSize += tf.getText().length(); } catch (IOException e) { getProject().handleException(e); } } } } toRefreshLocal.removeAll(synced); for (String toRemove : toRefreshLocal) { doc.delFile(toRemove); } // perform actual synchronization with the server sendFiles(doc); } finally { writeLock.unlock(); } } @Override public void synchronizeFile(ITernFile file) throws IOException { writeLock.lock(); try { TernFile tf = file.toTernServerFile(getProject()); String oldText = sentFiles.get(tf.getName()); if (tf.getText().equals(oldText) && !uploader.cancel(tf.getName())) { // no need to synchronize the file, already up-to-date return; } TernDoc doc = new TernDoc(); doc.addFile(tf); sendFiles(doc); } finally { writeLock.unlock(); } } @Override public void synchronizeFile(TernDoc doc, ITernFile file) throws IOException { writeLock.lock(); try { try { TernQuery query = doc.getQuery(); if (query != null) { if (TernResourcesManager.isJSFile(file)) { addJSFile(doc, file); return; } else if (TernResourcesManager.isHTMLFile(file)) { // This is HTML file case: never keep the file on the server String queryType = query.getType(); if (TernCompletionsQuery.isQueryType(queryType) || TernDefinitionQuery.isQueryType(queryType) || query instanceof TernLintQuery ) { addHTMLFile(doc, file); return; } } } TernFile tf = file.toTernServerFile(getProject()); String oldText = sentFiles.get(tf.getName()); if (tf.getText().equals(oldText) && !uploader.cancel(tf.getName())) { // no need to synchronize the file, already up-to-date return; } doc.addFile(tf); } finally { updateSentFiles(doc); // as this is // wait a bit for the sync to finish uploader.join(TIMEOUT); } } finally { writeLock.unlock(); } } protected void addJSFile(TernDoc doc, ITernFile file) throws IOException { TernQuery query = doc.getQuery(); String fileName = file.getFullName(getProject()); query.setFile(fileName); TernFile tf = file.toTernServerFile(getProject()); doc.addFile(tf); } protected void addHTMLFile(TernDoc doc, ITernFile file) throws IOException { TernQuery query = doc.getQuery(); TernFile tf = file.toTernServerFile(getProject()); doc.addFile(tf); query.set("file", "#" + (doc.getFiles().size() - 1)); //$NON-NLS-1$ //$NON-NLS-2$ } @Override public void synchronizeScriptPath(ITernScriptPath path, String... forced) { TernDoc doc = new TernDoc(); writeLock.lock(); try { // make sure we do not send duplicate files Set<String> requestedFiles = new HashSet<String>(sentFiles.keySet()); Set<String> perPath = new HashSet<String>(); syncedFilesPerPath.put(path, perPath); requestedFiles.removeAll(Arrays.asList(forced)); long totalSize = 0; for (String file : requestedFiles) { totalSize += sentFiles.get(file).length(); } for (ITernScriptResource resource : path.getScriptResources()) { // limit the number of files being sent to the Tern server if (totalSize >= MAX_ALLOWED_SIZE) { sizeExceeded(); break; } ITernFile file = resource.getFile(); if (file == null) { continue; } String name = file.getFullName(getProject()); perPath.add(name); if (!requestedFiles.contains(name)) { try { TernFile tf = file.toTernServerFile(getProject()); doc.addFile(tf); totalSize += tf.getText().length(); requestedFiles.add(name); } catch (IOException e) { getProject().handleException(e); } } } // perform actual synchronization with the server sendFiles(doc); } finally { writeLock.unlock(); } } private void updateSentFiles(TernDoc doc) { for (JsonValue value : doc.getFiles()) { if (value instanceof TernFile) { TernFile file = (TernFile) value; if (file.isType(FileType.full)) { String contents = file.getText(); if (StringUtils.isEmpty(contents)) { // treat file with empty contents as removed sentFiles.remove(file.getName()); } else { sentFiles.put(file.getName(), contents); } } } } } protected void sendFiles(TernDoc doc) { if (doc.hasFiles()) { updateSentFiles(doc); // sync is performed asynchronously uploader.request(doc); } } protected String getSentFileContent(String file) { readLock.lock(); try { return sentFiles.get(file); } finally { readLock.unlock(); } } @Override public void dispose() { cleanIndexedFiles(); } }