summaryrefslogtreecommitdiff
path: root/src/main/java/org/javacs/FileStore.java
blob: 61488a760e094d6a99104b7259e44b368564cdf4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
package org.javacs;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.javacs.lsp.DidChangeTextDocumentParams;
import org.javacs.lsp.DidCloseTextDocumentParams;
import org.javacs.lsp.DidOpenTextDocumentParams;
import org.javacs.lsp.TextDocumentContentChangeEvent;

class FileStore {

    private static final Set<Path> workspaceRoots = new HashSet<>();

    private static final Map<URI, VersionedContent> activeDocuments = new HashMap<>();

    /** javaSources[file] is the javaSources time of a .java source file. */
    // TODO organize by package name for speed of list(...)
    private static final TreeMap<Path, Info> javaSources = new TreeMap<>();

    private static class Info {
        final Instant modified;
        final String packageName;

        Info(Instant modified, String packageName) {
            this.modified = modified;
            this.packageName = packageName;
        }
    }

    static void setWorkspaceRoots(Set<Path> newRoots) {
        newRoots = normalize(newRoots);
        for (var root : workspaceRoots) {
            if (!newRoots.contains(root)) {
                workspaceRoots.removeIf(f -> f.startsWith(root));
            }
        }
        for (var root : newRoots) {
            if (!workspaceRoots.contains(root)) {
                addFiles(root);
            }
        }
        workspaceRoots.clear();
        workspaceRoots.addAll(newRoots);
    }

    private static Set<Path> normalize(Set<Path> newRoots) {
        var normalize = new HashSet<Path>();
        for (var root : newRoots) {
            normalize.add(root.toAbsolutePath().normalize());
        }
        return normalize;
    }

    private static void addFiles(Path root) {
        try {
            Files.walk(root).filter(FileStore::isJavaFile).forEach(FileStore::readInfoFromDisk);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    static Collection<Path> all() {
        return javaSources.keySet();
    }

    static List<Path> list(String packageName) {
        var list = new ArrayList<Path>();
        for (var kv : javaSources.entrySet()) {
            var file = kv.getKey();
            var info = kv.getValue();
            if (info.packageName.equals(packageName)) list.add(file);
        }
        return list;
    }

    static boolean contains(Path file) {
        return isJavaFile(file) && javaSources.containsKey(file);
    }

    static Instant modified(Path file) {
        // If we've never checked before, look up modified time on disk
        if (!javaSources.containsKey(file)) {
            readInfoFromDisk(file);
        }

        // Look up modified time from cache
        return javaSources.get(file).modified;
    }

    static String packageName(Path file) {
        // If we've never checked before, look up modified time on disk
        if (!javaSources.containsKey(file)) {
            readInfoFromDisk(file);
        }

        // Look up modified time from cache
        return javaSources.get(file).packageName;
    }

    static String suggestedPackageName(Path file) {
        var sourceRoot = sourceRoot(file);
        var relativePath = sourceRoot.relativize(file).getParent();
        if (relativePath == null) return "";
        return relativePath.toString().replace(File.separatorChar, '.');
    }

    private static Path sourceRoot(Path file) {
        for (var dir = file.getParent(); dir != null; dir = dir.getParent()) {
            for (var related : javaSourcesIn(dir)) {
                if (related.equals(file)) continue;
                var packageName = packageName(related);
                var relativePath = Paths.get(packageName.replace('.', File.separatorChar));
                var sourceRoot = dir;
                for (var i = 0; i < relativePath.getNameCount(); i++) {
                    sourceRoot = sourceRoot.getParent();
                }
                return sourceRoot;
            }
        }
        return file.getParent();
    }

    private static List<Path> javaSourcesIn(Path dir) {
        var tail = javaSources.tailMap(dir, false);
        var list = new ArrayList<Path>();
        for (var file : tail.keySet()) {
            if (!file.startsWith(dir)) break;
            list.add(file);
        }
        return list;
    }

    static void externalCreate(Path file) {
        readInfoFromDisk(file);
    }

    static void externalChange(Path file) {
        readInfoFromDisk(file);
    }

    static void externalDelete(Path file) {
        javaSources.remove(file);
    }

    private static void readInfoFromDisk(Path file) {
        try {
            var time = Files.getLastModifiedTime(file).toInstant();
            var packageName = Parser.packageName(file);
            javaSources.put(file, new Info(time, packageName));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    static void open(DidOpenTextDocumentParams params) {
        var document = params.textDocument;
        var uri = document.uri;
        if (!isJavaFile(uri)) return;
        activeDocuments.put(uri, new VersionedContent(document.text, document.version));
    }

    static void change(DidChangeTextDocumentParams params) {
        var document = params.textDocument;
        var uri = document.uri;
        if (isJavaFile(uri)) {
            var existing = activeDocuments.get(uri);
            var newText = existing.content;

            if (document.version > existing.version) {
                for (var change : params.contentChanges) {
                    if (change.range == null) newText = change.text;
                    else newText = patch(newText, change);
                }

                activeDocuments.put(uri, new VersionedContent(newText, document.version));
            } else LOG.warning("Ignored change with version " + document.version + " <= " + existing.version);
        }
    }

    static void close(DidCloseTextDocumentParams params) {
        var document = params.textDocument;
        var uri = document.uri;
        if (isJavaFile(uri)) {
            // Remove from source cache
            activeDocuments.remove(uri);
        }
    }

    static Set<URI> activeDocuments() {
        return activeDocuments.keySet();
    }

    static int version(URI file) {
        if (!activeDocuments.containsKey(file)) return -1;
        return activeDocuments.get(file).version;
    }

    static String contents(URI file) {
        if (!isJavaFile(file)) {
            throw new RuntimeException(file + " is not a java file");
        }
        if (activeDocuments.containsKey(file)) {
            return activeDocuments.get(file).content;
        }
        try {
            // TODO I think there is a faster path here
            return Files.readAllLines(Paths.get(file)).stream().collect(Collectors.joining("\n"));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    static String contents(Path file) {
        return contents(file.toUri());
    }

    static InputStream inputStream(Path file) {
        var uri = file.toUri();
        if (activeDocuments.containsKey(uri)) {
            var string = activeDocuments.get(uri).content;
            var bytes = string.getBytes();
            return new ByteArrayInputStream(bytes);
        }
        try {
            return Files.newInputStream(file);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    static BufferedReader bufferedReader(Path file) {
        var uri = file.toUri();
        if (activeDocuments.containsKey(uri)) {
            var string = activeDocuments.get(uri).content;
            return new BufferedReader(new StringReader(string));
        }
        try {
            return Files.newBufferedReader(file);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    static BufferedReader lines(Path file) {
        try {
            return Files.newBufferedReader(file);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private static String patch(String sourceText, TextDocumentContentChangeEvent change) {
        try {
            var range = change.range;
            var reader = new BufferedReader(new StringReader(sourceText));
            var writer = new StringWriter();

            // Skip unchanged lines
            int line = 0;

            while (line < range.start.line) {
                writer.write(reader.readLine() + '\n');
                line++;
            }

            // Skip unchanged chars
            for (int character = 0; character < range.start.character; character++) writer.write(reader.read());

            // Write replacement text
            writer.write(change.text);

            // Skip replaced text
            reader.skip(change.rangeLength);

            // Write remaining text
            while (true) {
                int next = reader.read();

                if (next == -1) return writer.toString();
                else writer.write(next);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    static boolean isJavaFile(Path file) {
        var name = file.getFileName().toString();
        // We hide module-info.java from javac, because when javac sees module-info.java
        // it goes into "module mode" and starts looking for classes on the module class path.
        // This becomes evident when javac starts recompiling *way too much* on each task,
        // because it doesn't realize there are already up-to-date .class files.
        // The better solution would be for java-language server to detect the presence of module-info.java,
        // and go into its own "module mode" where it infers a module source path and a module class path.
        return name.endsWith(".java") && !name.equals("module-info.java");
    }

    static boolean isJavaFile(URI uri) {
        return uri.getScheme().equals("file") && isJavaFile(Paths.get(uri));
    }

    private static Set<Path> allJavaFilesInDirs(Set<Path> dirs) {
        var all = new HashSet<Path>();
        for (var dir : dirs) {
            try {
                Files.walk(dir).filter(FileStore::isJavaFile).forEach(all::add);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        return all;
    }

    private static Set<Path> sourcePath(Set<Path> workspaceRoots) {
        LOG.info("Searching for source roots in " + workspaceRoots);

        var certaintyThreshold = 10;
        var sourceRoots = new HashMap<Path, Integer>();
        fileLoop:
        for (var file : allJavaFilesInDirs(workspaceRoots)) {
            // First, check if we already have a high-confidence source root containing file
            for (var root : sourceRoots.keySet()) {
                var confidence = sourceRoots.get(root);
                if (file.startsWith(root) && confidence > certaintyThreshold) {
                    continue fileLoop;
                }
            }
            // Otherwise, parse the file and look at its package declaration
            var parse = Parser.parse(file);
            var packageName = Objects.toString(parse.getPackageName(), "");
            var packagePath = packageName.replace('.', File.separatorChar);
            // If file has no package declaration, don't try to guess a source root
            // This is probably a new file that the user will add a package declaration to later
            if (packagePath.isEmpty()) {
                LOG.warning("Ignoring file with missing package declaration " + file);
                continue fileLoop;
            }
            // If path to file contradicts package declaration, give up
            var dir = file.getParent();
            if (!dir.endsWith(packagePath)) {
                LOG.warning("Java source file " + file + " is not in " + packagePath);
                continue fileLoop;
            }
            // Otherwise, use the package declaration to guess the source root
            var up = Paths.get(packagePath).getNameCount();
            var sourceRoot = dir;
            for (int i = 0; i < up; i++) {
                sourceRoot = sourceRoot.getParent();
            }
            // Increment our confidence in sourceRoot as a source root
            var count = sourceRoots.getOrDefault(sourceRoot, 0);
            sourceRoots.put(sourceRoot, count + 1);
        }
        return sourceRoots.keySet();
    }

    private static final Logger LOG = Logger.getLogger("main");
}