tests to specify when modification count for file is advanced and when it isn't advanced
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / vfs / newvfs / persistent / FSRecords.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.vfs.newvfs.persistent;
17
18 import com.intellij.openapi.application.ApplicationManager;
19 import com.intellij.openapi.application.ApplicationNamesInfo;
20 import com.intellij.openapi.application.PathManager;
21 import com.intellij.openapi.diagnostic.Logger;
22 import com.intellij.openapi.util.Disposer;
23 import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream;
24 import com.intellij.openapi.util.io.ByteSequence;
25 import com.intellij.openapi.util.io.FileAttributes;
26 import com.intellij.openapi.util.io.FileUtil;
27 import com.intellij.openapi.vfs.newvfs.FileAttribute;
28 import com.intellij.openapi.vfs.newvfs.impl.FileNameCache;
29 import com.intellij.util.ArrayUtil;
30 import com.intellij.util.BitUtil;
31 import com.intellij.util.CompressionUtil;
32 import com.intellij.util.SystemProperties;
33 import com.intellij.util.concurrency.AppExecutorUtil;
34 import com.intellij.util.containers.ConcurrentIntObjectMap;
35 import com.intellij.util.containers.ContainerUtil;
36 import com.intellij.util.containers.IntArrayList;
37 import com.intellij.util.io.*;
38 import com.intellij.util.io.DataOutputStream;
39 import com.intellij.util.io.storage.*;
40 import gnu.trove.TIntArrayList;
41 import org.jetbrains.annotations.NotNull;
42 import org.jetbrains.annotations.Nullable;
43 import org.jetbrains.annotations.TestOnly;
44
45 import javax.swing.*;
46 import java.awt.*;
47 import java.io.*;
48 import java.nio.charset.Charset;
49 import java.security.MessageDigest;
50 import java.util.Arrays;
51 import java.util.concurrent.ConcurrentMap;
52 import java.util.concurrent.ExecutorService;
53 import java.util.concurrent.ScheduledFuture;
54 import java.util.concurrent.locks.ReentrantReadWriteLock;
55
56 /**
57  * @author max
58  */
59 @SuppressWarnings({"PointlessArithmeticExpression", "HardCodedStringLiteral"})
60 public class FSRecords {
61   private static final Logger LOG = Logger.getInstance("#com.intellij.vfs.persistent.FSRecords");
62
63   public static final boolean weHaveContentHashes = SystemProperties.getBooleanProperty("idea.share.contents", true);
64   private static final boolean lazyVfsDataCleaning = SystemProperties.getBooleanProperty("idea.lazy.vfs.data.cleaning", true);
65   static final boolean backgroundVfsFlush = SystemProperties.getBooleanProperty("idea.background.vfs.flush", true);
66   public static final boolean persistentAttributesList = SystemProperties.getBooleanProperty("idea.persistent.attr.list", true);
67   private static final boolean inlineAttributes = SystemProperties.getBooleanProperty("idea.inline.vfs.attributes", true);
68   static final boolean bulkAttrReadSupport = SystemProperties.getBooleanProperty("idea.bulk.attr.read", false);
69   static final boolean useSnappyForCompression = SystemProperties.getBooleanProperty("idea.use.snappy.for.vfs", false);
70   private static final boolean useSmallAttrTable = SystemProperties.getBooleanProperty("idea.use.small.attr.table.for.vfs", true);
71   static final String VFS_FILES_EXTENSION = System.getProperty("idea.vfs.files.extension", ".dat");
72
73   private static final int VERSION = 21 + (weHaveContentHashes ? 0x10:0) + (IOUtil.ourByteBuffersUseNativeByteOrder ? 0x37:0) +
74                                      (persistentAttributesList ? 31 : 0) + (bulkAttrReadSupport ? 0x27:0) + (inlineAttributes ? 0x31 : 0) +
75                                      (useSnappyForCompression ? 0x7f : 0) + (useSmallAttrTable ? 0x31 : 0) +
76                                      (PersistentHashMapValueStorage.COMPRESSION_ENABLED ? 21:0);
77
78   private static final int PARENT_OFFSET = 0;
79   private static final int PARENT_SIZE = 4;
80   private static final int NAME_OFFSET = PARENT_OFFSET + PARENT_SIZE;
81   private static final int NAME_SIZE = 4;
82   private static final int FLAGS_OFFSET = NAME_OFFSET + NAME_SIZE;
83   private static final int FLAGS_SIZE = 4;
84   private static final int ATTR_REF_OFFSET = FLAGS_OFFSET + FLAGS_SIZE;
85   private static final int ATTR_REF_SIZE = 4;
86   private static final int CONTENT_OFFSET = ATTR_REF_OFFSET + ATTR_REF_SIZE;
87   private static final int CONTENT_SIZE = 4;
88   private static final int TIMESTAMP_OFFSET = CONTENT_OFFSET + CONTENT_SIZE;
89   private static final int TIMESTAMP_SIZE = 8;
90   private static final int MOD_COUNT_OFFSET = TIMESTAMP_OFFSET + TIMESTAMP_SIZE;
91   private static final int MOD_COUNT_SIZE = 4;
92   private static final int LENGTH_OFFSET = MOD_COUNT_OFFSET + MOD_COUNT_SIZE;
93   private static final int LENGTH_SIZE = 8;
94
95   private static final int RECORD_SIZE = LENGTH_OFFSET + LENGTH_SIZE;
96
97   private static final byte[] ZEROES = new byte[RECORD_SIZE];
98
99   private static final int HEADER_VERSION_OFFSET = 0;
100   //private static final int HEADER_RESERVED_4BYTES_OFFSET = 4; // reserved
101   private static final int HEADER_GLOBAL_MOD_COUNT_OFFSET = 8;
102   private static final int HEADER_CONNECTION_STATUS_OFFSET = 12;
103   private static final int HEADER_TIMESTAMP_OFFSET = 16;
104   private static final int HEADER_SIZE = HEADER_TIMESTAMP_OFFSET + 8;
105
106   private static final int CONNECTED_MAGIC = 0x12ad34e4;
107   private static final int SAFELY_CLOSED_MAGIC = 0x1f2f3f4f;
108   private static final int CORRUPTED_MAGIC = 0xabcf7f7f;
109
110   private static final FileAttribute ourChildrenAttr = new FileAttribute("FsRecords.DIRECTORY_CHILDREN");
111
112   private static final ReentrantReadWriteLock.ReadLock r;
113   private static final ReentrantReadWriteLock.WriteLock w;
114
115   private static volatile int ourLocalModificationCount;
116   private static volatile boolean ourIsDisposed;
117
118   private static final int FREE_RECORD_FLAG = 0x100;
119   private static final int ALL_VALID_FLAGS = PersistentFS.ALL_VALID_FLAGS | FREE_RECORD_FLAG;
120
121   static {
122     //noinspection ConstantConditions
123     assert HEADER_SIZE <= RECORD_SIZE;
124
125     ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
126     r = lock.readLock();
127     w = lock.writeLock();
128   }
129
130   static void writeAttributesToRecord(int id, int parentId, @NotNull FileAttributes attributes, @NotNull String name) {
131     w.lock();
132     try {
133       setName(id, name);
134
135       setTimestamp(id, attributes.lastModified);
136       setLength(id, attributes.isDirectory() ? -1L : attributes.length);
137
138       setFlags(id, (attributes.isDirectory() ? PersistentFS.IS_DIRECTORY_FLAG : 0) |
139                              (attributes.isWritable() ? 0 : PersistentFS.IS_READ_ONLY) |
140                              (attributes.isSymLink() ? PersistentFS.IS_SYMLINK : 0) |
141                              (attributes.isSpecial() ? PersistentFS.IS_SPECIAL : 0) |
142                              (attributes.isHidden() ? PersistentFS.IS_HIDDEN : 0), true);
143       setParent(id, parentId);
144     }
145     catch (Throwable e) {
146       DbConnection.handleError(e);
147     }
148     finally {
149       w.unlock();
150     }
151   }
152
153   static void requestVfsRebuild(Throwable e) {
154     //noinspection ThrowableResultOfMethodCallIgnored
155     DbConnection.handleError(e);
156   }
157
158   static File basePath() {
159     return new File(DbConnection.getCachesDir());
160   }
161
162   public static class DbConnection {
163     private static boolean ourInitialized;
164     private static final ConcurrentMap<String, Integer> myAttributeIds = ContainerUtil.newConcurrentMap();
165
166     private static PersistentStringEnumerator myNames;
167     private static Storage myAttributes;
168     private static RefCountingStorage myContents;
169     private static ResizeableMappedFile myRecords;
170     private static PersistentBTreeEnumerator<byte[]> myContentHashesEnumerator;
171     private static final VfsDependentEnum<String> myAttributesList =
172       new VfsDependentEnum<>("attrib", EnumeratorStringDescriptor.INSTANCE, 1);
173     private static final TIntArrayList myFreeRecords = new TIntArrayList();
174
175     private static boolean myDirty;
176     private static ScheduledFuture<?> myFlushingFuture;
177     private static boolean myCorrupted;
178
179     private static final AttrPageAwareCapacityAllocationPolicy REASONABLY_SMALL = new AttrPageAwareCapacityAllocationPolicy();
180
181
182     public static void connect() {
183       w.lock();
184       try {
185         if (!ourInitialized) {
186           init();
187           setupFlushing();
188           ourInitialized = true;
189         }
190       }
191       finally {
192         w.unlock();
193       }
194     }
195
196     private static void scanFreeRecords() {
197       final int filelength = (int)myRecords.length();
198       LOG.assertTrue(filelength % RECORD_SIZE == 0, "invalid file size: " + filelength);
199
200       int count = filelength / RECORD_SIZE;
201       for (int n = 2; n < count; n++) {
202         if (BitUtil.isSet(getFlags(n), FREE_RECORD_FLAG)) {
203           myFreeRecords.add(n);
204         }
205       }
206     }
207
208     static int getFreeRecord() {
209       if (myFreeRecords.isEmpty()) return 0;
210       return myFreeRecords.remove(myFreeRecords.size() - 1);
211     }
212
213     private static void createBrokenMarkerFile(@Nullable Throwable reason) {
214       final File brokenMarker = getCorruptionMarkerFile();
215
216       try {
217         final ByteArrayOutputStream out = new ByteArrayOutputStream();
218         try (PrintStream stream = new PrintStream(out)) {
219           new Exception().printStackTrace(stream);
220           if (reason != null) {
221             stream.print("\nReason:\n");
222             reason.printStackTrace(stream);
223           }
224         }
225         LOG.info("Creating VFS corruption marker; Trace=\n" + out);
226
227         try (FileWriter writer = new FileWriter(brokenMarker)) {
228           writer.write("These files are corrupted and must be rebuilt from the scratch on next startup");
229         }
230       }
231       catch (IOException e) {
232         // No luck.
233       }
234     }
235
236     private static File getCorruptionMarkerFile() {
237       return new File(basePath(), "corruption.marker");
238     }
239
240     private static void init() {
241       final File basePath = basePath().getAbsoluteFile();
242       basePath.mkdirs();
243
244       final File namesFile = new File(basePath, "names" + VFS_FILES_EXTENSION);
245       final File attributesFile = new File(basePath, "attrib" + VFS_FILES_EXTENSION);
246       final File contentsFile = new File(basePath, "content" + VFS_FILES_EXTENSION);
247       final File contentsHashesFile = new File(basePath, "contentHashes" + VFS_FILES_EXTENSION);
248       final File recordsFile = new File(basePath, "records" + VFS_FILES_EXTENSION);
249
250       final File vfsDependentEnumBaseFile = VfsDependentEnum.getBaseFile();
251
252       if (!namesFile.exists()) {
253         invalidateIndex("'" + namesFile.getPath() + "' does not exist");
254       }
255
256       try {
257         if (getCorruptionMarkerFile().exists()) {
258           invalidateIndex("corruption marker found");
259           throw new IOException("Corruption marker file found");
260         }
261
262         PagedFileStorage.StorageLockContext storageLockContext = new PagedFileStorage.StorageLockContext(false);
263         myNames = new PersistentStringEnumerator(namesFile, storageLockContext);
264
265         myAttributes = new Storage(attributesFile.getPath(), REASONABLY_SMALL) {
266           @Override
267           protected AbstractRecordsTable createRecordsTable(PagePool pool, File recordsFile) throws IOException {
268             return inlineAttributes && useSmallAttrTable ? new CompactRecordsTable(recordsFile, pool, false) : super.createRecordsTable(pool, recordsFile);
269           }
270         };
271         myContents = new RefCountingStorage(contentsFile.getPath(), CapacityAllocationPolicy.FIVE_PERCENT_FOR_GROWTH, useSnappyForCompression) {
272           @NotNull
273           @Override
274           protected ExecutorService createExecutor() {
275             return AppExecutorUtil.createBoundedApplicationPoolExecutor("FSRecords pool",1);
276           }
277         }; // sources usually zipped with 4x ratio
278         myContentHashesEnumerator = weHaveContentHashes ? new ContentHashesUtil.HashEnumerator(contentsHashesFile, storageLockContext): null;
279         boolean aligned = PagedFileStorage.BUFFER_SIZE % RECORD_SIZE == 0;
280         assert aligned; // for performance
281         myRecords = new ResizeableMappedFile(recordsFile, 20 * 1024, storageLockContext,
282                                              PagedFileStorage.BUFFER_SIZE, aligned, IOUtil.ourByteBuffersUseNativeByteOrder);
283
284         if (myRecords.length() == 0) {
285           cleanRecord(0); // Clean header
286           cleanRecord(1); // Create root record
287           setCurrentVersion();
288         }
289
290         if (getVersion() != VERSION) {
291           throw new IOException("FS repository version mismatch");
292         }
293
294         if (myRecords.getInt(HEADER_CONNECTION_STATUS_OFFSET) != SAFELY_CLOSED_MAGIC) {
295           throw new IOException("FS repository wasn't safely shut down");
296         }
297         markDirty();
298         scanFreeRecords();
299       }
300       catch (Exception e) { // IOException, IllegalArgumentException
301         LOG.info("Filesystem storage is corrupted or does not exist. [Re]Building. Reason: " + e.getMessage());
302         try {
303           closeFiles();
304
305           boolean deleted = FileUtil.delete(getCorruptionMarkerFile());
306           deleted &= IOUtil.deleteAllFilesStartingWith(namesFile);
307           deleted &= AbstractStorage.deleteFiles(attributesFile.getPath());
308           deleted &= AbstractStorage.deleteFiles(contentsFile.getPath());
309           deleted &= IOUtil.deleteAllFilesStartingWith(contentsHashesFile);
310           deleted &= IOUtil.deleteAllFilesStartingWith(recordsFile);
311           deleted &= IOUtil.deleteAllFilesStartingWith(vfsDependentEnumBaseFile);
312
313           if (!deleted) {
314             throw new IOException("Cannot delete filesystem storage files");
315           }
316         }
317         catch (final IOException e1) {
318           final Runnable warnAndShutdown = () -> {
319             if (ApplicationManager.getApplication().isUnitTestMode()) {
320               //noinspection CallToPrintStackTrace
321               e1.printStackTrace();
322             }
323             else {
324               final String message = "Files in " + basePath.getPath() + " are locked.\n" +
325                                      ApplicationNamesInfo.getInstance().getProductName() + " will not be able to start up.";
326               if (!ApplicationManager.getApplication().isHeadlessEnvironment()) {
327                 JOptionPane.showMessageDialog(JOptionPane.getRootFrame(), message, "Fatal Error", JOptionPane.ERROR_MESSAGE);
328               }
329               else {
330                 //noinspection UseOfSystemOutOrSystemErr
331                 System.err.println(message);
332               }
333             }
334             Runtime.getRuntime().halt(1);
335           };
336
337           if (EventQueue.isDispatchThread()) {
338             warnAndShutdown.run();
339           }
340           else {
341             //noinspection SSBasedInspection
342             SwingUtilities.invokeLater(warnAndShutdown);
343           }
344
345           throw new RuntimeException("Can't rebuild filesystem storage ", e1);
346         }
347
348         init();
349       }
350     }
351
352     private static void invalidateIndex(String reason) {
353       LOG.info("Marking VFS as corrupted: " + reason);
354       final File indexRoot = PathManager.getIndexRoot();
355       if (indexRoot.exists()) {
356         final String[] children = indexRoot.list();
357         if (children != null && children.length > 0) {
358           // create index corruption marker only if index directory exists and is non-empty
359           // It is incorrect to consider non-existing indices "corrupted"
360           FileUtil.createIfDoesntExist(new File(PathManager.getIndexRoot(), "corruption.marker"));
361         }
362       }
363     }
364
365     private static String getCachesDir() {
366       String dir = System.getProperty("caches_dir");
367       return dir == null ? PathManager.getSystemPath() + "/caches/" : dir;
368     }
369
370     private static void markDirty() {
371       if (!myDirty) {
372         myDirty = true;
373         myRecords.putInt(HEADER_CONNECTION_STATUS_OFFSET, CONNECTED_MAGIC);
374       }
375     }
376
377     private static void setupFlushing() {
378       if (!backgroundVfsFlush)
379         return;
380
381       myFlushingFuture = FlushingDaemon.everyFiveSeconds(new Runnable() {
382         private int lastModCount;
383
384         @Override
385         public void run() {
386           if (lastModCount == ourLocalModificationCount) {
387             flush();
388           }
389           lastModCount = ourLocalModificationCount;
390         }
391       });
392     }
393
394     public static void force() {
395       w.lock();
396       try {
397         doForce();
398       }
399       finally {
400         w.unlock();
401       }
402     }
403
404     private static void doForce() {
405       if (myNames != null) {
406         myNames.force();
407         myAttributes.force();
408         myContents.force();
409         if (myContentHashesEnumerator != null) myContentHashesEnumerator.force();
410         markClean();
411         myRecords.force();
412       }
413     }
414
415     private static void flush() {
416       if (!isDirty() || HeavyProcessLatch.INSTANCE.isRunning()) return;
417
418       r.lock();
419       try {
420         if (myFlushingFuture == null) {
421           return; // avoid NPE when close has already taken place
422         }
423         doForce();
424       }
425       finally {
426         r.unlock();
427       }
428     }
429
430     public static boolean isDirty() {
431       return myDirty || myNames.isDirty() || myAttributes.isDirty() || myContents.isDirty() || myRecords.isDirty() ||
432              myContentHashesEnumerator != null && myContentHashesEnumerator.isDirty();
433     }
434
435
436     private static int getVersion() {
437       final int recordsVersion = myRecords.getInt(HEADER_VERSION_OFFSET);
438       if (myAttributes.getVersion() != recordsVersion || myContents.getVersion() != recordsVersion) return -1;
439
440       return recordsVersion;
441     }
442
443     public static long getTimestamp() {
444       return myRecords.getLong(HEADER_TIMESTAMP_OFFSET);
445     }
446
447     private static void setCurrentVersion() {
448       myRecords.putInt(HEADER_VERSION_OFFSET, VERSION);
449       myRecords.putLong(HEADER_TIMESTAMP_OFFSET, System.currentTimeMillis());
450       myAttributes.setVersion(VERSION);
451       myContents.setVersion(VERSION);
452       myRecords.putInt(HEADER_CONNECTION_STATUS_OFFSET, SAFELY_CLOSED_MAGIC);
453     }
454
455     static void cleanRecord(int id) {
456       myRecords.put(id * RECORD_SIZE, ZEROES, 0, RECORD_SIZE);
457     }
458
459     public static PersistentStringEnumerator getNames() {
460       return myNames;
461     }
462
463     private static void closeFiles() throws IOException {
464       if (myFlushingFuture != null) {
465         myFlushingFuture.cancel(false);
466         myFlushingFuture = null;
467       }
468
469       if (myNames != null) {
470         myNames.close();
471         myNames = null;
472       }
473
474       if (myAttributes != null) {
475         Disposer.dispose(myAttributes);
476         myAttributes = null;
477       }
478
479       if (myContents != null) {
480         Disposer.dispose(myContents);
481         myContents = null;
482       }
483
484       if (myContentHashesEnumerator != null) {
485         myContentHashesEnumerator.close();
486         myContentHashesEnumerator = null;
487       }
488
489       if (myRecords != null) {
490         markClean();
491         myRecords.close();
492         myRecords = null;
493       }
494       ourInitialized = false;
495     }
496
497     private static void markClean() {
498       if (myDirty) {
499         myDirty = false;
500         myRecords.putInt(HEADER_CONNECTION_STATUS_OFFSET, myCorrupted ? CORRUPTED_MAGIC : SAFELY_CLOSED_MAGIC);
501       }
502     }
503
504     private static final int RESERVED_ATTR_ID = bulkAttrReadSupport ? 1 : 0;
505     private static final int FIRST_ATTR_ID_OFFSET = bulkAttrReadSupport ? RESERVED_ATTR_ID : 0;
506
507     private static int getAttributeId(@NotNull String attId) throws IOException {
508       if (persistentAttributesList) {
509         return myAttributesList.getId(attId) + FIRST_ATTR_ID_OFFSET;
510       }
511       Integer integer = myAttributeIds.get(attId);
512       if (integer != null) return integer.intValue();
513       int enumeratedId = myNames.enumerate(attId);
514       integer = myAttributeIds.putIfAbsent(attId, enumeratedId);
515       return integer == null ? enumeratedId:  integer.intValue();
516     }
517
518     private static void handleError(@NotNull Throwable e) throws RuntimeException, Error {
519       if (!ourIsDisposed) {
520         // No need to forcibly mark VFS corrupted if it is already shut down
521         if (!myCorrupted && w.tryLock()) { // avoid deadlock if r lock is occupied by current thread
522           w.unlock();
523           createBrokenMarkerFile(e);
524           myCorrupted = true;
525           force();
526         }
527       }
528
529       if (e instanceof Error) throw (Error)e;
530       if (e instanceof RuntimeException) throw (RuntimeException)e;
531       throw new RuntimeException(e);
532     }
533
534     private static class AttrPageAwareCapacityAllocationPolicy extends CapacityAllocationPolicy {
535       boolean myAttrPageRequested;
536
537       @Override
538       public int calculateCapacity(int requiredLength) {   // 20% for growth
539         return Math.max(myAttrPageRequested ? 8:32, Math.min((int)(requiredLength * 1.2), (requiredLength / 1024 + 1) * 1024));
540       }
541     }
542   }
543
544   private FSRecords() {
545   }
546
547   public static void connect() {
548     DbConnection.connect();
549   }
550
551   public static long getCreationTimestamp() {
552     r.lock();
553     try {
554       return DbConnection.getTimestamp();
555     }
556     finally {
557       r.unlock();
558     }
559   }
560
561   private static ResizeableMappedFile getRecords() {
562     return DbConnection.myRecords;
563   }
564
565   private static PersistentBTreeEnumerator<byte[]> getContentHashesEnumerator() {
566     return DbConnection.myContentHashesEnumerator;
567   }
568
569   private static RefCountingStorage getContentStorage() {
570     return DbConnection.myContents;
571   }
572
573   private static Storage getAttributesStorage() {
574     return DbConnection.myAttributes;
575   }
576
577   public static PersistentStringEnumerator getNames() {
578     return DbConnection.getNames();
579   }
580
581   // todo: Address  / capacity store in records table, size store with payload
582   public static int createRecord() {
583     w.lock();
584     try {
585       DbConnection.markDirty();
586
587       final int free = DbConnection.getFreeRecord();
588       if (free == 0) {
589         final int fileLength = length();
590         LOG.assertTrue(fileLength % RECORD_SIZE == 0);
591         int newRecord = fileLength / RECORD_SIZE;
592         DbConnection.cleanRecord(newRecord);
593         assert fileLength + RECORD_SIZE == length();
594         return newRecord;
595       }
596       else {
597         if (lazyVfsDataCleaning) deleteContentAndAttributes(free);
598         DbConnection.cleanRecord(free);
599         return free;
600       }
601     }
602     catch (Throwable e) {
603       DbConnection.handleError(e);
604     }
605     finally {
606       w.unlock();
607     }
608     return -1;
609   }
610
611   private static int length() {
612     return (int)getRecords().length();
613   }
614   public static int getMaxId() {
615     r.lock();
616     try {
617       return length()/RECORD_SIZE;
618     }
619     finally {
620       r.unlock();
621     }
622   }
623
624   static void deleteRecordRecursively(int id) {
625     w.lock();
626     try {
627       incModCount(id);
628       if (lazyVfsDataCleaning) {
629         markAsDeletedRecursively(id);
630       } else {
631         doDeleteRecursively(id);
632       }
633     }
634     catch (Throwable e) {
635       DbConnection.handleError(e);
636     }
637     finally {
638       w.unlock();
639     }
640   }
641
642   private static void markAsDeletedRecursively(final int id) {
643     for (int subrecord : list(id)) {
644       markAsDeletedRecursively(subrecord);
645     }
646
647     markAsDeleted(id);
648   }
649
650   private static void markAsDeleted(final int id) {
651     w.lock();
652     try {
653       DbConnection.markDirty();
654       addToFreeRecordsList(id);
655     }
656     catch (Throwable e) {
657       DbConnection.handleError(e);
658     }
659     finally {
660       w.unlock();
661     }
662   }
663
664   private static void doDeleteRecursively(final int id) {
665     for (int subrecord : list(id)) {
666       doDeleteRecursively(subrecord);
667     }
668
669     deleteRecord(id);
670   }
671
672   private static void deleteRecord(final int id) {
673     w.lock();
674     try {
675       DbConnection.markDirty();
676       deleteContentAndAttributes(id);
677
678       DbConnection.cleanRecord(id);
679       addToFreeRecordsList(id);
680     }
681     catch (Throwable e) {
682       DbConnection.handleError(e);
683     }
684     finally {
685       w.unlock();
686     }
687   }
688
689   private static void deleteContentAndAttributes(int id) throws IOException {
690     int content_page = getContentRecordId(id);
691     if (content_page != 0) {
692       if (weHaveContentHashes) {
693         getContentStorage().releaseRecord(content_page, false);
694       } else {
695         getContentStorage().releaseRecord(content_page);
696       }
697     }
698
699     int att_page = getAttributeRecordId(id);
700     if (att_page != 0) {
701       final DataInputStream attStream = getAttributesStorage().readStream(att_page);
702       if (bulkAttrReadSupport) skipRecordHeader(attStream, DbConnection.RESERVED_ATTR_ID, id);
703
704       while (attStream.available() > 0) {
705         DataInputOutputUtil.readINT(attStream);// Attribute ID;
706         int attAddressOrSize = DataInputOutputUtil.readINT(attStream);
707
708         if (inlineAttributes) {
709           if(attAddressOrSize < MAX_SMALL_ATTR_SIZE) {
710             attStream.skipBytes(attAddressOrSize);
711             continue;
712           }
713           attAddressOrSize -= MAX_SMALL_ATTR_SIZE;
714         }
715         getAttributesStorage().deleteRecord(attAddressOrSize);
716       }
717       attStream.close();
718       getAttributesStorage().deleteRecord(att_page);
719     }
720   }
721
722   private static void addToFreeRecordsList(int id) {
723     // DbConnection.addFreeRecord(id); // important! Do not add fileId to free list until restart
724     setFlags(id, FREE_RECORD_FLAG, false);
725   }
726
727   static int[] listRoots() {
728     try {
729       r.lock();
730       try {
731         final DataInputStream input = readAttribute(1, ourChildrenAttr);
732         if (input == null) return ArrayUtil.EMPTY_INT_ARRAY;
733
734         try {
735           final int count = DataInputOutputUtil.readINT(input);
736           int[] result = ArrayUtil.newIntArray(count);
737           int prevId = 0;
738           for (int i = 0; i < count; i++) {
739             DataInputOutputUtil.readINT(input); // Name
740             prevId = result[i] = DataInputOutputUtil.readINT(input) + prevId; // Id
741           }
742           return result;
743         }
744         finally {
745           input.close();
746         }
747       }
748       finally {
749         r.unlock();
750       }
751     }
752     catch (Throwable e) {
753       DbConnection.handleError(e);
754       return null;
755     }
756   }
757
758   @TestOnly
759   public static void force() {
760     DbConnection.force();
761   }
762
763   @TestOnly
764   public static boolean isDirty() {
765     return DbConnection.isDirty();
766   }
767
768   private static void saveNameIdSequenceWithDeltas(int[] names, int[] ids, DataOutputStream output) throws IOException {
769     DataInputOutputUtil.writeINT(output, names.length);
770     int prevId = 0;
771     int prevNameId = 0;
772     for (int i = 0; i < names.length; i++) {
773       DataInputOutputUtil.writeINT(output, names[i] - prevNameId);
774       DataInputOutputUtil.writeINT(output, ids[i] - prevId);
775       prevId = ids[i];
776       prevNameId = names[i];
777     }
778   }
779
780   static int findRootRecord(@NotNull String rootUrl) {
781     w.lock();
782
783     try {
784       DbConnection.markDirty();
785       final int root = getNames().enumerate(rootUrl);
786
787       final DataInputStream input = readAttribute(1, ourChildrenAttr);
788       int[] names = ArrayUtil.EMPTY_INT_ARRAY;
789       int[] ids = ArrayUtil.EMPTY_INT_ARRAY;
790
791       if (input != null) {
792         try {
793           final int count = DataInputOutputUtil.readINT(input);
794           names = ArrayUtil.newIntArray(count);
795           ids = ArrayUtil.newIntArray(count);
796           int prevId = 0;
797           int prevNameId = 0;
798
799           for (int i = 0; i < count; i++) {
800             final int name = DataInputOutputUtil.readINT(input) + prevNameId;
801             final int id = DataInputOutputUtil.readINT(input) + prevId;
802             if (name == root) {
803               return id;
804             }
805
806             prevNameId = names[i] = name;
807             prevId = ids[i] = id;
808           }
809         }
810         finally {
811           input.close();
812         }
813       }
814
815       int id;
816       try (DataOutputStream output = writeAttribute(1, ourChildrenAttr)) {
817         id = createRecord();
818
819         int index = Arrays.binarySearch(ids, id);
820         ids = ArrayUtil.insert(ids, -index - 1, id);
821         names = ArrayUtil.insert(names, -index - 1, root);
822
823         saveNameIdSequenceWithDeltas(names, ids, output);
824       }
825
826       return id;
827     }
828     catch (Throwable e) {
829       DbConnection.handleError(e);
830     }
831     finally {
832       w.unlock();
833     }
834     return -1;
835   }
836
837   static void deleteRootRecord(int id) {
838     w.lock();
839
840     try {
841       DbConnection.markDirty();
842       final DataInputStream input = readAttribute(1, ourChildrenAttr);
843       assert input != null;
844       int[] names;
845       int[] ids;
846       try {
847         int count = DataInputOutputUtil.readINT(input);
848
849         names = ArrayUtil.newIntArray(count);
850         ids = ArrayUtil.newIntArray(count);
851         int prevId = 0;
852         int prevNameId = 0;
853         for (int i = 0; i < count; i++) {
854           names[i] = DataInputOutputUtil.readINT(input) + prevNameId;
855           ids[i] = DataInputOutputUtil.readINT(input) + prevId;
856           prevId = ids[i];
857           prevNameId = names[i];
858         }
859       }
860       finally {
861         input.close();
862       }
863
864       final int index = ArrayUtil.find(ids, id);
865       assert index >= 0;
866
867       names = ArrayUtil.remove(names, index);
868       ids = ArrayUtil.remove(ids, index);
869
870       try (DataOutputStream output = writeAttribute(1, ourChildrenAttr)) {
871         saveNameIdSequenceWithDeltas(names, ids, output);
872       }
873     }
874     catch (Throwable e) {
875       DbConnection.handleError(e);
876     }
877     finally {
878       w.unlock();
879     }
880   }
881
882   @NotNull
883   public static int[] list(int id) {
884     try {
885       r.lock();
886       try {
887         final DataInputStream input = readAttribute(id, ourChildrenAttr);
888         if (input == null) return ArrayUtil.EMPTY_INT_ARRAY;
889
890         final int count = DataInputOutputUtil.readINT(input);
891         final int[] result = ArrayUtil.newIntArray(count);
892         int prevId = id;
893         for (int i = 0; i < count; i++) {
894           prevId = result[i] = DataInputOutputUtil.readINT(input) + prevId;
895         }
896         input.close();
897         return result;
898       }
899       finally {
900         r.unlock();
901       }
902     }
903     catch (Throwable e) {
904       DbConnection.handleError(e);
905       return ArrayUtil.EMPTY_INT_ARRAY;
906     }
907   }
908
909   public static class NameId {
910     @NotNull
911     public static final NameId[] EMPTY_ARRAY = new NameId[0];
912     public final int id;
913     public final CharSequence name;
914     public final int nameId;
915
916     public NameId(int id, int nameId, @NotNull CharSequence name) {
917       this.id = id;
918       this.nameId = nameId;
919       this.name = name;
920     }
921
922     @Override
923     public String toString() {
924       return name + " (" + id + ")";
925     }
926   }
927
928   @NotNull
929   public static NameId[] listAll(int parentId) {
930     try {
931       r.lock();
932       try {
933         final DataInputStream input = readAttribute(parentId, ourChildrenAttr);
934         if (input == null) return NameId.EMPTY_ARRAY;
935
936         int count = DataInputOutputUtil.readINT(input);
937         NameId[] result = count == 0 ? NameId.EMPTY_ARRAY : new NameId[count];
938         int prevId = parentId;
939         for (int i = 0; i < count; i++) {
940           int id = DataInputOutputUtil.readINT(input) + prevId;
941           prevId = id;
942           int nameId = getNameId(id);
943           result[i] = new NameId(id, nameId, FileNameCache.getVFileName(nameId));
944         }
945         input.close();
946         return result;
947       }
948       finally {
949         r.unlock();
950       }
951     }
952     catch (Throwable e) {
953       DbConnection.handleError(e);
954       return NameId.EMPTY_ARRAY;
955     }
956   }
957
958   static boolean wereChildrenAccessed(int id) {
959     try {
960       r.lock();
961       try {
962         return findAttributePage(id, ourChildrenAttr, false) != 0;
963       } finally {
964         r.unlock();
965       }
966     }
967     catch (Throwable e) {
968       DbConnection.handleError(e);
969     }
970     return false;
971   }
972
973   public static void updateList(int id, @NotNull int[] childIds) {
974     Arrays.sort(childIds);
975     w.lock();
976     try {
977       DbConnection.markDirty();
978       try (DataOutputStream record = writeAttribute(id, ourChildrenAttr)) {
979         DataInputOutputUtil.writeINT(record, childIds.length);
980
981         int prevId = id;
982         for (int childId : childIds) {
983           assert childId > 0 : childId;
984           if (childId == id) {
985             LOG.error("Cyclic parent child relations");
986           }
987           else {
988             int delta = childId - prevId;
989             DataInputOutputUtil.writeINT(record, delta);
990             prevId = childId;
991           }
992         }
993       }
994     }
995     catch (Throwable e) {
996       DbConnection.handleError(e);
997     }
998     finally {
999       w.unlock();
1000     }
1001   }
1002
1003   private static void incModCount(int id) {
1004     incLocalModCount();
1005     final int count = getModCount() + 1;
1006     getRecords().putInt(HEADER_GLOBAL_MOD_COUNT_OFFSET, count);
1007
1008     setModCount(id, count);
1009   }
1010
1011   private static void incLocalModCount() {
1012     DbConnection.markDirty();
1013     ourLocalModificationCount++;
1014   }
1015
1016   static int getLocalModCount() {
1017     return ourLocalModificationCount; // This is volatile, only modified under Application.runWriteAction() lock.
1018   }
1019
1020   public static int getModCount() {
1021     r.lock();
1022     try {
1023       return getRecords().getInt(HEADER_GLOBAL_MOD_COUNT_OFFSET);
1024     }
1025     finally {
1026       r.unlock();
1027     }
1028   }
1029
1030   public static int getParent(int id) {
1031     try {
1032       r.lock();
1033       try {
1034         final int parentId = getRecordInt(id, PARENT_OFFSET);
1035         if (parentId == id) {
1036           LOG.error("Cyclic parent child relations in the database. id = " + id);
1037           return 0;
1038         }
1039
1040         return parentId;
1041       }
1042       finally {
1043         r.unlock();
1044       }
1045     }
1046     catch (Throwable e) {
1047       DbConnection.handleError(e);
1048     }
1049     return -1;
1050   }
1051
1052   // returns id, parent(id), parent(parent(id)), ...  (already cached id or rootId)
1053   @NotNull
1054   public static TIntArrayList getParents(int id, @NotNull ConcurrentIntObjectMap<?> idCache) {
1055     TIntArrayList result = new TIntArrayList(10);
1056     r.lock();
1057     try {
1058       int parentId;
1059       do {
1060         result.add(id);
1061         if (idCache.containsKey(id)) {
1062           break;
1063         }
1064         parentId = getRecordInt(id, PARENT_OFFSET);
1065         if (parentId == id || result.size() % 128 == 0 && result.contains(parentId)) {
1066           LOG.error("Cyclic parent child relations in the database. id = " + parentId);
1067           return result;
1068         }
1069         id = parentId;
1070       } while (parentId != 0);
1071     }
1072     catch (Throwable e) {
1073       DbConnection.handleError(e);
1074     }
1075     finally {
1076       r.unlock();
1077     }
1078     return result;
1079   }
1080
1081   public static void setParent(int id, int parentId) {
1082     if (id == parentId) {
1083       LOG.error("Cyclic parent/child relations");
1084       return;
1085     }
1086
1087     w.lock();
1088     try {
1089       incModCount(id);
1090       putRecordInt(id, PARENT_OFFSET, parentId);
1091     }
1092     catch (Throwable e) {
1093       DbConnection.handleError(e);
1094     }
1095     finally {
1096       w.unlock();
1097     }
1098   }
1099
1100   public static int getNameId(int id) {
1101     try {
1102       r.lock();
1103       try {
1104         return getRecordInt(id, NAME_OFFSET);
1105       }
1106       finally {
1107         r.unlock();
1108       }
1109     }
1110     catch (Throwable e) {
1111       DbConnection.handleError(e);
1112     }
1113     return -1;
1114   }
1115
1116   public static int getNameId(String name) {
1117     try {
1118       r.lock();
1119       try {
1120         return getNames().enumerate(name);
1121       }
1122       finally {
1123         r.unlock();
1124       }
1125     }
1126     catch (Throwable e) {
1127       DbConnection.handleError(e);
1128     }
1129     return -1;
1130   }
1131
1132   public static String getName(int id) {
1133     return getNameSequence(id).toString();
1134   }
1135
1136   @NotNull
1137   public static CharSequence getNameSequence(int id) {
1138     try {
1139       r.lock();
1140       try {
1141         final int nameId = getRecordInt(id, NAME_OFFSET);
1142         return nameId == 0 ? "" : FileNameCache.getVFileName(nameId);
1143       }
1144       finally {
1145         r.unlock();
1146       }
1147     }
1148     catch (Throwable e) {
1149       DbConnection.handleError(e);
1150       return "";
1151     }
1152   }
1153
1154   public static String getNameByNameId(int nameId) {
1155     try {
1156       r.lock();
1157       try {
1158         return nameId != 0 ? getNames().valueOf(nameId) : "";
1159       }
1160       finally {
1161         r.unlock();
1162       }
1163     }
1164     catch (Throwable e) {
1165       DbConnection.handleError(e);
1166     }
1167     return null;
1168   }
1169
1170   public static void setName(int id, @NotNull String name) {
1171     w.lock();
1172     try {
1173       incModCount(id);
1174       int nameId = getNames().enumerate(name);
1175       putRecordInt(id, NAME_OFFSET, nameId);
1176     }
1177     catch (Throwable e) {
1178       DbConnection.handleError(e);
1179     }
1180     finally {
1181       w.unlock();
1182     }
1183   }
1184
1185   public static int getFlags(int id) {
1186     r.lock();
1187     try {
1188       return getRecordInt(id, FLAGS_OFFSET);
1189     }
1190     finally {
1191       r.unlock();
1192     }
1193   }
1194
1195   public static void setFlags(int id, int flags, final boolean markAsChange) {
1196     w.lock();
1197     try {
1198       if (markAsChange) {
1199         incModCount(id);
1200       }
1201       putRecordInt(id, FLAGS_OFFSET, flags);
1202     }
1203     catch (Throwable e) {
1204       DbConnection.handleError(e);
1205     }
1206     finally {
1207       w.unlock();
1208     }
1209   }
1210
1211   public static long getLength(int id) {
1212     r.lock();
1213     try {
1214       return getRecords().getLong(getOffset(id, LENGTH_OFFSET));
1215     }
1216     finally {
1217       r.unlock();
1218     }
1219   }
1220
1221   public static void setLength(int id, long len) {
1222     w.lock();
1223     try {
1224       ResizeableMappedFile records = getRecords();
1225       int lengthOffset = getOffset(id, LENGTH_OFFSET);
1226       if (records.getLong(lengthOffset) != len) {
1227         incModCount(id);
1228         records.putLong(lengthOffset, len);
1229       }
1230     }
1231     catch (Throwable e) {
1232       DbConnection.handleError(e);
1233     }
1234     finally {
1235       w.unlock();
1236     }
1237   }
1238
1239   public static long getTimestamp(int id) {
1240     r.lock();
1241     try {
1242       return getRecords().getLong(getOffset(id, TIMESTAMP_OFFSET));
1243     }
1244     finally {
1245       r.unlock();
1246     }
1247   }
1248
1249   public static void setTimestamp(int id, long value) {
1250     w.lock();
1251     try {
1252       int timeStampOffset = getOffset(id, TIMESTAMP_OFFSET);
1253       ResizeableMappedFile records = getRecords();
1254       if (records.getLong(timeStampOffset) != value) {
1255         incModCount(id);
1256         records.putLong(timeStampOffset, value);
1257       }
1258     }
1259     catch (Throwable e) {
1260       DbConnection.handleError(e);
1261     }
1262     finally {
1263       w.unlock();
1264     }
1265   }
1266
1267   static int getModCount(int id) {
1268     r.lock();
1269     try {
1270       return getRecordInt(id, MOD_COUNT_OFFSET);
1271     }
1272     finally {
1273       r.unlock();
1274     }
1275   }
1276
1277   private static void setModCount(int id, int value) {
1278     putRecordInt(id, MOD_COUNT_OFFSET, value);
1279   }
1280
1281   private static int getContentRecordId(int fileId) {
1282     return getRecordInt(fileId, CONTENT_OFFSET);
1283   }
1284
1285   private static void setContentRecordId(int id, int value) {
1286     putRecordInt(id, CONTENT_OFFSET, value);
1287   }
1288
1289   private static int getAttributeRecordId(int id) {
1290     return getRecordInt(id, ATTR_REF_OFFSET);
1291   }
1292
1293   private static void setAttributeRecordId(int id, int value) {
1294     putRecordInt(id, ATTR_REF_OFFSET, value);
1295   }
1296
1297   private static int getRecordInt(int id, int offset) {
1298     return getRecords().getInt(getOffset(id, offset));
1299   }
1300
1301   private static void putRecordInt(int id, int offset, int value) {
1302     getRecords().putInt(getOffset(id, offset), value);
1303   }
1304
1305   private static int getOffset(int id, int offset) {
1306     return id * RECORD_SIZE + offset;
1307   }
1308
1309   @Nullable
1310   public static DataInputStream readContent(int fileId) {
1311     try {
1312       r.lock();
1313       int page;
1314       try {
1315         checkFileIsValid(fileId);
1316
1317         page = getContentRecordId(fileId);
1318         if (page == 0) return null;
1319       }
1320       finally {
1321         r.unlock();
1322       }
1323       return doReadContentById(page);
1324     }
1325     catch (Throwable e) {
1326       DbConnection.handleError(e);
1327     }
1328     return null;
1329   }
1330
1331   @Nullable
1332   static DataInputStream readContentById(int contentId) {
1333     try {
1334       return doReadContentById(contentId);
1335     }
1336     catch (Throwable e) {
1337       DbConnection.handleError(e);
1338     }
1339     return null;
1340   }
1341
1342   private static DataInputStream doReadContentById(int contentId) throws IOException {
1343     DataInputStream stream = getContentStorage().readStream(contentId);
1344     if (useSnappyForCompression) {
1345       byte[] bytes = CompressionUtil.readCompressed(stream);
1346       stream = new DataInputStream(new ByteArrayInputStream(bytes));
1347     }
1348
1349     return stream;
1350   }
1351
1352   @Nullable
1353   public static DataInputStream readAttributeWithLock(int fileId, FileAttribute att) {
1354     try {
1355       r.lock();
1356       try {
1357         DataInputStream stream = readAttribute(fileId, att);
1358         if (stream != null && att.isVersioned()) {
1359           try {
1360             int actualVersion = DataInputOutputUtil.readINT(stream);
1361             if (actualVersion != att.getVersion()) {
1362               stream.close();
1363               return null;
1364             }
1365           }
1366           catch (IOException e) {
1367             stream.close();
1368             return null;
1369           }
1370         }
1371         return stream;
1372       }
1373       finally {
1374         r.unlock();
1375       }
1376     }
1377     catch (Throwable e) {
1378       DbConnection.handleError(e);
1379     }
1380     return null;
1381   }
1382
1383   // should be called under r or w lock
1384   @Nullable
1385   private static DataInputStream readAttribute(int fileId, FileAttribute attribute) throws IOException {
1386     checkFileIsValid(fileId);
1387
1388     int recordId = getAttributeRecordId(fileId);
1389     if (recordId == 0) return null;
1390     int encodedAttrId = DbConnection.getAttributeId(attribute.getId());
1391
1392     Storage storage = getAttributesStorage();
1393
1394     int page = 0;
1395
1396     try (DataInputStream attrRefs = storage.readStream(recordId)) {
1397       if (bulkAttrReadSupport) skipRecordHeader(attrRefs, DbConnection.RESERVED_ATTR_ID, fileId);
1398
1399       while (attrRefs.available() > 0) {
1400         final int attIdOnPage = DataInputOutputUtil.readINT(attrRefs);
1401         final int attrAddressOrSize = DataInputOutputUtil.readINT(attrRefs);
1402
1403         if (attIdOnPage != encodedAttrId) {
1404           if (inlineAttributes && attrAddressOrSize < MAX_SMALL_ATTR_SIZE) {
1405             attrRefs.skipBytes(attrAddressOrSize);
1406           }
1407         }
1408         else {
1409           if (inlineAttributes && attrAddressOrSize < MAX_SMALL_ATTR_SIZE) {
1410             byte[] b = new byte[attrAddressOrSize];
1411             attrRefs.readFully(b);
1412             return new DataInputStream(new ByteArrayInputStream(b));
1413           }
1414           page = inlineAttributes ? attrAddressOrSize - MAX_SMALL_ATTR_SIZE : attrAddressOrSize;
1415           break;
1416         }
1417       }
1418     }
1419
1420     if (page == 0) {
1421       return null;
1422     }
1423     DataInputStream stream = getAttributesStorage().readStream(page);
1424     if (bulkAttrReadSupport) skipRecordHeader(stream, encodedAttrId, fileId);
1425     return stream;
1426   }
1427
1428   // Vfs small attrs: store inline:
1429   // file's AttrId -> [size, capacity] attr record (RESERVED_ATTR_ID fileId)? (attrId ((smallAttrSize smallAttrData) | (attr record)) )
1430   // other attr record: (AttrId, fileId) ? attrData
1431   private static final int MAX_SMALL_ATTR_SIZE = 64;
1432
1433   private static int findAttributePage(int fileId, FileAttribute attr, boolean toWrite) throws IOException {
1434     checkFileIsValid(fileId);
1435
1436     int recordId = getAttributeRecordId(fileId);
1437     int encodedAttrId = DbConnection.getAttributeId(attr.getId());
1438     boolean directoryRecord = false;
1439
1440     Storage storage = getAttributesStorage();
1441
1442     if (recordId == 0) {
1443       if (!toWrite) return 0;
1444
1445       recordId = storage.createNewRecord();
1446       setAttributeRecordId(fileId, recordId);
1447       directoryRecord = true;
1448     }
1449     else {
1450       try (DataInputStream attrRefs = storage.readStream(recordId)) {
1451         if (bulkAttrReadSupport) skipRecordHeader(attrRefs, DbConnection.RESERVED_ATTR_ID, fileId);
1452
1453         while (attrRefs.available() > 0) {
1454           final int attIdOnPage = DataInputOutputUtil.readINT(attrRefs);
1455           final int attrAddressOrSize = DataInputOutputUtil.readINT(attrRefs);
1456
1457           if (attIdOnPage == encodedAttrId) {
1458             if (inlineAttributes) {
1459               return attrAddressOrSize < MAX_SMALL_ATTR_SIZE ? -recordId : attrAddressOrSize - MAX_SMALL_ATTR_SIZE;
1460             }
1461             else {
1462               return attrAddressOrSize;
1463             }
1464           }
1465           else {
1466             if (inlineAttributes && attrAddressOrSize < MAX_SMALL_ATTR_SIZE) {
1467               attrRefs.skipBytes(attrAddressOrSize);
1468             }
1469           }
1470         }
1471       }
1472     }
1473
1474     if (toWrite) {
1475       Storage.AppenderStream appender = storage.appendStream(recordId);
1476       if (bulkAttrReadSupport) {
1477         if (directoryRecord) {
1478           DataInputOutputUtil.writeINT(appender, DbConnection.RESERVED_ATTR_ID);
1479           DataInputOutputUtil.writeINT(appender, fileId);
1480         }
1481       }
1482
1483       DataInputOutputUtil.writeINT(appender, encodedAttrId);
1484       int attrAddress = storage.createNewRecord();
1485       DataInputOutputUtil.writeINT(appender, inlineAttributes ? attrAddress + MAX_SMALL_ATTR_SIZE : attrAddress);
1486       DbConnection.REASONABLY_SMALL.myAttrPageRequested = true;
1487       try {
1488         appender.close();
1489       } finally {
1490         DbConnection.REASONABLY_SMALL.myAttrPageRequested = false;
1491       }
1492       return attrAddress;
1493     }
1494
1495     return 0;
1496   }
1497
1498   private static void skipRecordHeader(DataInputStream refs, int expectedRecordTag, int expectedFileId) throws IOException {
1499     int attId = DataInputOutputUtil.readINT(refs);// attrId
1500     assert attId == expectedRecordTag || expectedRecordTag == 0;
1501     int fileId = DataInputOutputUtil.readINT(refs);// fileId
1502     assert expectedFileId == fileId || expectedFileId == 0;
1503   }
1504
1505   private static void writeRecordHeader(int recordTag, int fileId, DataOutputStream appender) throws IOException {
1506     DataInputOutputUtil.writeINT(appender, recordTag);
1507     DataInputOutputUtil.writeINT(appender, fileId);
1508   }
1509
1510   private static void checkFileIsValid(int fileId) {
1511     assert fileId > 0 : fileId;
1512     // TODO: This assertion is a bit timey, will remove when bug is caught.
1513     if (!lazyVfsDataCleaning) {
1514       assert !BitUtil.isSet(getFlags(fileId), FREE_RECORD_FLAG) : "Accessing attribute of a deleted page: " + fileId + ":" + getName(fileId);
1515     }
1516   }
1517
1518   static int acquireFileContent(int fileId) {
1519     w.lock();
1520     try {
1521       int record = getContentRecordId(fileId);
1522       if (record > 0) getContentStorage().acquireRecord(record);
1523       return record;
1524     }
1525     catch (Throwable e) {
1526       DbConnection.handleError(e);
1527     }
1528     finally {
1529       w.unlock();
1530     }
1531     return -1;
1532   }
1533
1534   static void releaseContent(int contentId) {
1535     w.lock();
1536     try {
1537       RefCountingStorage contentStorage = getContentStorage();
1538       if (weHaveContentHashes) {
1539         contentStorage.releaseRecord(contentId, false);
1540       } else {
1541         contentStorage.releaseRecord(contentId);
1542       }
1543     }
1544     catch (Throwable e) {
1545       DbConnection.handleError(e);
1546     }
1547     finally {
1548       w.unlock();
1549     }
1550   }
1551
1552   public static int getContentId(int fileId) {
1553     try {
1554       r.lock();
1555       try {
1556         return getContentRecordId(fileId);
1557       }
1558       finally {
1559         r.unlock();
1560       }
1561     }
1562     catch (Throwable e) {
1563       DbConnection.handleError(e);
1564     }
1565     return -1;
1566   }
1567
1568   @NotNull
1569   static DataOutputStream writeContent(int fileId, boolean readOnly) {
1570     return new ContentOutputStream(fileId, readOnly);
1571   }
1572
1573   private static final MessageDigest myDigest = ContentHashesUtil.createHashDigest();
1574
1575   static void writeContent(int fileId, ByteSequence bytes, boolean readOnly) {
1576     try {
1577       new ContentOutputStream(fileId, readOnly).writeBytes(bytes);
1578     }
1579     catch (Throwable e) {
1580       DbConnection.handleError(e);
1581     }
1582   }
1583
1584   static int storeUnlinkedContent(byte[] bytes) {
1585     w.lock();
1586     try {
1587       int recordId;
1588
1589       if (weHaveContentHashes) {
1590         recordId = findOrCreateContentRecord(bytes, 0, bytes.length);
1591         if (recordId > 0) return recordId;
1592         recordId = -recordId;
1593       } else {
1594         recordId = getContentStorage().acquireNewRecord();
1595       }
1596       AbstractStorage.StorageDataOutput output = getContentStorage().writeStream(recordId, true);
1597       output.write(bytes);
1598       output.close();
1599       return recordId;
1600     }
1601     catch (IOException e) {
1602       DbConnection.handleError(e);
1603     }
1604     finally {
1605       w.unlock();
1606     }
1607     return -1;
1608   }
1609
1610   @NotNull
1611   public static DataOutputStream writeAttribute(final int fileId, @NotNull FileAttribute att) {
1612     DataOutputStream stream = new AttributeOutputStream(fileId, att);
1613     if (att.isVersioned()) {
1614       try {
1615         DataInputOutputUtil.writeINT(stream, att.getVersion());
1616       }
1617       catch (IOException e) {
1618         throw new RuntimeException(e);
1619       }
1620     }
1621     return stream;
1622   }
1623
1624   private static class ContentOutputStream extends DataOutputStream {
1625     final int myFileId;
1626     final boolean myFixedSize;
1627
1628     private ContentOutputStream(final int fileId, boolean readOnly) {
1629       super(new BufferExposingByteArrayOutputStream());
1630       myFileId = fileId;
1631       myFixedSize = readOnly;
1632     }
1633
1634     @Override
1635     public void close() throws IOException {
1636       super.close();
1637
1638       try {
1639         final BufferExposingByteArrayOutputStream _out = (BufferExposingByteArrayOutputStream)out;
1640         writeBytes(new ByteSequence(_out.getInternalBuffer(), 0, _out.size()));
1641       }
1642       catch (Throwable e) {
1643         DbConnection.handleError(e);
1644       }
1645     }
1646
1647     public void writeBytes(ByteSequence bytes) throws IOException {
1648       RefCountingStorage contentStorage = getContentStorage();
1649       w.lock();
1650       try {
1651         checkFileIsValid(myFileId);
1652
1653         int page;
1654         final boolean fixedSize;
1655         if (weHaveContentHashes) {
1656           page = findOrCreateContentRecord(bytes.getBytes(), bytes.getOffset(), bytes.getLength());
1657
1658           if (page < 0 || getContentId(myFileId) != page) {
1659             incModCount(myFileId);
1660             setContentRecordId(myFileId, page > 0 ? page : -page);
1661           }
1662
1663           setContentRecordId(myFileId, page > 0 ? page : -page);
1664
1665           if (page > 0) return;
1666           page = -page;
1667           fixedSize = true;
1668         } else {
1669           incModCount(myFileId);
1670           page = getContentRecordId(myFileId);
1671           if (page == 0 || contentStorage.getRefCount(page) > 1) {
1672             page = contentStorage.acquireNewRecord();
1673             setContentRecordId(myFileId, page);
1674           }
1675           fixedSize = myFixedSize;
1676         }
1677
1678         if (useSnappyForCompression) {
1679           BufferExposingByteArrayOutputStream out = new BufferExposingByteArrayOutputStream();
1680           DataOutputStream outputStream = new DataOutputStream(out);
1681           byte[] rawBytes = bytes.getBytes();
1682           if (bytes.getOffset() != 0) {
1683             rawBytes = new byte[bytes.getLength()];
1684             System.arraycopy(bytes.getBytes(), bytes.getOffset(), rawBytes, 0, bytes.getLength());
1685           }
1686           CompressionUtil.writeCompressed(outputStream, rawBytes, bytes.getLength());
1687           outputStream.close();
1688           bytes = new ByteSequence(out.getInternalBuffer(), 0, out.size());
1689         }
1690         contentStorage.writeBytes(page, bytes, fixedSize);
1691       }
1692       finally {
1693         w.unlock();
1694       }
1695     }
1696   }
1697
1698   private static final boolean DO_HARD_CONSISTENCY_CHECK = false;
1699   private static final boolean DUMP_STATISTICS = weHaveContentHashes;  // TODO: remove once not needed
1700   private static long totalContents;
1701   private static long totalReuses;
1702   private static long time;
1703   private static int contents;
1704   private static int reuses;
1705
1706   private static int findOrCreateContentRecord(byte[] bytes, int offset, int length) throws IOException {
1707     assert weHaveContentHashes;
1708
1709     long started = DUMP_STATISTICS ? System.nanoTime():0;
1710     myDigest.reset();
1711     myDigest.update(String.valueOf(length - offset).getBytes(Charset.defaultCharset()));
1712     myDigest.update("\0".getBytes(Charset.defaultCharset()));
1713     myDigest.update(bytes, offset, length);
1714     byte[] digest = myDigest.digest();
1715     long done = DUMP_STATISTICS ? System.nanoTime() - started : 0;
1716     time += done;
1717
1718     ++contents;
1719     totalContents += length;
1720
1721     if (DUMP_STATISTICS && (contents & 0x3FFF) == 0) {
1722       LOG.info("Contents:" + contents + " of " + totalContents + ", reuses:" + reuses + " of " + totalReuses + " for " + time / 1000000);
1723     }
1724     PersistentBTreeEnumerator<byte[]> hashesEnumerator = getContentHashesEnumerator();
1725     final int largestId = hashesEnumerator.getLargestId();
1726     int page = hashesEnumerator.enumerate(digest);
1727
1728     if (page <= largestId) {
1729       ++reuses;
1730       getContentStorage().acquireRecord(page);
1731       totalReuses += length;
1732
1733       if (DO_HARD_CONSISTENCY_CHECK) {
1734         DataInputStream stream = doReadContentById(page);
1735         int i = offset;
1736         for(int c = 0; c < length; ++c) {
1737           if (stream.available() == 0) {
1738             assert false;
1739           }
1740           if (bytes[i++] != stream.readByte()) {
1741             assert false;
1742           }
1743         }
1744         if (stream.available() > 0) {
1745           assert false;
1746         }
1747       }
1748       return page;
1749     } else {
1750       int newRecord = getContentStorage().acquireNewRecord();
1751       if (page != newRecord) {
1752         assert false:"Unexpected content storage modification";
1753       }
1754       if (DO_HARD_CONSISTENCY_CHECK) {
1755         if (hashesEnumerator.enumerate(digest) != page) {
1756           assert false;
1757         }
1758
1759         byte[] bytes1 = hashesEnumerator.valueOf(page);
1760         if (!Arrays.equals(digest, bytes1)) {
1761           assert false;
1762         }
1763       }
1764       return -page;
1765     }
1766   }
1767
1768   private static class AttributeOutputStream extends DataOutputStream {
1769     private final FileAttribute myAttribute;
1770     private final int myFileId;
1771
1772     private AttributeOutputStream(final int fileId, @NotNull FileAttribute attribute) {
1773       super(new BufferExposingByteArrayOutputStream());
1774       myFileId = fileId;
1775       myAttribute = attribute;
1776     }
1777
1778     @Override
1779     public void close() throws IOException {
1780       super.close();
1781
1782       try {
1783         final BufferExposingByteArrayOutputStream _out = (BufferExposingByteArrayOutputStream)out;
1784
1785         if (inlineAttributes && _out.size() < MAX_SMALL_ATTR_SIZE) {
1786           w.lock();
1787           try {
1788             rewriteDirectoryRecordWithAttrContent(_out);
1789             incLocalModCount();
1790           }
1791           finally {
1792             w.unlock();
1793           }
1794         }
1795         else {
1796           w.lock();
1797           try {
1798             incLocalModCount();
1799             int page = findAttributePage(myFileId, myAttribute, true);
1800             if (inlineAttributes && page < 0) {
1801               rewriteDirectoryRecordWithAttrContent(new BufferExposingByteArrayOutputStream());
1802               page = findAttributePage(myFileId, myAttribute, true);
1803             }
1804
1805             if (bulkAttrReadSupport) {
1806               BufferExposingByteArrayOutputStream stream = new BufferExposingByteArrayOutputStream();
1807               out = stream;
1808               writeRecordHeader(DbConnection.getAttributeId(myAttribute.getId()), myFileId, this);
1809               write(_out.getInternalBuffer(), 0, _out.size());
1810               getAttributesStorage().writeBytes(page, new ByteSequence(stream.getInternalBuffer(), 0, stream.size()), myAttribute.isFixedSize());
1811             }
1812             else {
1813               getAttributesStorage().writeBytes(page, new ByteSequence(_out.getInternalBuffer(), 0, _out.size()), myAttribute.isFixedSize());
1814             }
1815           }
1816           finally {
1817             w.unlock();
1818           }
1819         }
1820       }
1821       catch (Throwable e) {
1822         DbConnection.handleError(e);
1823       }
1824     }
1825
1826     void rewriteDirectoryRecordWithAttrContent(BufferExposingByteArrayOutputStream _out) throws IOException {
1827       int recordId = getAttributeRecordId(myFileId);
1828       assert inlineAttributes;
1829       int encodedAttrId = DbConnection.getAttributeId(myAttribute.getId());
1830
1831       Storage storage = getAttributesStorage();
1832       BufferExposingByteArrayOutputStream unchangedPreviousDirectoryStream = null;
1833       boolean directoryRecord = false;
1834
1835
1836       if (recordId == 0) {
1837         recordId = storage.createNewRecord();
1838         setAttributeRecordId(myFileId, recordId);
1839         directoryRecord = true;
1840       }
1841       else {
1842         DataInputStream attrRefs = storage.readStream(recordId);
1843
1844         DataOutputStream dataStream = null;
1845
1846         try {
1847           final int remainingAtStart = attrRefs.available();
1848           if (bulkAttrReadSupport) {
1849             unchangedPreviousDirectoryStream = new BufferExposingByteArrayOutputStream();
1850             dataStream = new DataOutputStream(unchangedPreviousDirectoryStream);
1851             int attId = DataInputOutputUtil.readINT(attrRefs);
1852             assert attId == DbConnection.RESERVED_ATTR_ID;
1853             int fileId = DataInputOutputUtil.readINT(attrRefs);
1854             assert myFileId == fileId;
1855
1856             writeRecordHeader(attId, fileId, dataStream);
1857           }
1858           while (attrRefs.available() > 0) {
1859             final int attIdOnPage = DataInputOutputUtil.readINT(attrRefs);
1860             final int attrAddressOrSize = DataInputOutputUtil.readINT(attrRefs);
1861
1862             if (attIdOnPage != encodedAttrId) {
1863               if (dataStream == null) {
1864                 unchangedPreviousDirectoryStream = new BufferExposingByteArrayOutputStream();
1865                 dataStream = new DataOutputStream(unchangedPreviousDirectoryStream);
1866               }
1867               DataInputOutputUtil.writeINT(dataStream, attIdOnPage);
1868               DataInputOutputUtil.writeINT(dataStream, attrAddressOrSize);
1869
1870               if (attrAddressOrSize < MAX_SMALL_ATTR_SIZE) {
1871                 byte[] b = new byte[attrAddressOrSize];
1872                 attrRefs.readFully(b);
1873                 dataStream.write(b);
1874               }
1875             }
1876             else {
1877               if (attrAddressOrSize < MAX_SMALL_ATTR_SIZE) {
1878                 if (_out.size() == attrAddressOrSize) {
1879                   // update inplace when new attr has the same size
1880                   int remaining = attrRefs.available();
1881                   storage.replaceBytes(recordId, remainingAtStart - remaining, new ByteSequence(_out.getInternalBuffer(), 0, _out.size()));
1882                   return;
1883                 }
1884                 attrRefs.skipBytes(attrAddressOrSize);
1885               }
1886             }
1887           }
1888         }
1889         finally {
1890           attrRefs.close();
1891           if (dataStream != null) dataStream.close();
1892         }
1893       }
1894
1895       AbstractStorage.StorageDataOutput directoryStream = storage.writeStream(recordId);
1896       if (directoryRecord) {
1897         if (bulkAttrReadSupport) writeRecordHeader(DbConnection.RESERVED_ATTR_ID, myFileId, directoryStream);
1898       }
1899       if(unchangedPreviousDirectoryStream != null) {
1900         directoryStream.write(unchangedPreviousDirectoryStream.getInternalBuffer(), 0, unchangedPreviousDirectoryStream.size());
1901       }
1902       if (_out.size() > 0) {
1903         DataInputOutputUtil.writeINT(directoryStream, encodedAttrId);
1904         DataInputOutputUtil.writeINT(directoryStream, _out.size());
1905         directoryStream.write(_out.getInternalBuffer(), 0, _out.size());
1906       }
1907
1908       directoryStream.close();
1909     }
1910   }
1911
1912   public static void dispose() {
1913     w.lock();
1914     try {
1915       DbConnection.force();
1916       DbConnection.closeFiles();
1917     }
1918     catch (Throwable e) {
1919       DbConnection.handleError(e);
1920     }
1921     finally {
1922       ourIsDisposed = true;
1923       w.unlock();
1924     }
1925   }
1926
1927   public static void invalidateCaches() {
1928     DbConnection.createBrokenMarkerFile(null);
1929   }
1930
1931   static void checkSanity() {
1932     long t = System.currentTimeMillis();
1933
1934     r.lock();
1935     try {
1936       final int fileLength = length();
1937       assert fileLength % RECORD_SIZE == 0;
1938       int recordCount = fileLength / RECORD_SIZE;
1939
1940       IntArrayList usedAttributeRecordIds = new IntArrayList();
1941       IntArrayList validAttributeIds = new IntArrayList();
1942       for (int id = 2; id < recordCount; id++) {
1943         int flags = getFlags(id);
1944         LOG.assertTrue((flags & ~ALL_VALID_FLAGS) == 0, "Invalid flags: 0x" + Integer.toHexString(flags) + ", id: " + id);
1945         if (BitUtil.isSet(flags, FREE_RECORD_FLAG)) {
1946           LOG.assertTrue(DbConnection.myFreeRecords.contains(id), "Record, marked free, not in free list: " + id);
1947         }
1948         else {
1949           LOG.assertTrue(!DbConnection.myFreeRecords.contains(id), "Record, not marked free, in free list: " + id);
1950           checkRecordSanity(id, recordCount, usedAttributeRecordIds, validAttributeIds);
1951         }
1952       }
1953     }
1954     finally {
1955       r.unlock();
1956     }
1957
1958     t = System.currentTimeMillis() - t;
1959     LOG.info("Sanity check took " + t + " ms");
1960   }
1961
1962   private static void checkRecordSanity(final int id, final int recordCount, final IntArrayList usedAttributeRecordIds,
1963                                         final IntArrayList validAttributeIds) {
1964     int parentId = getParent(id);
1965     assert parentId >= 0 && parentId < recordCount;
1966     if (parentId > 0 && getParent(parentId) > 0) {
1967       int parentFlags = getFlags(parentId);
1968       assert !BitUtil.isSet(parentFlags, FREE_RECORD_FLAG) : parentId + ": " + Integer.toHexString(parentFlags);
1969       assert BitUtil.isSet(parentFlags, PersistentFS.IS_DIRECTORY_FLAG) : parentId + ": " + Integer.toHexString(parentFlags);
1970     }
1971
1972     String name = getName(id);
1973     LOG.assertTrue(parentId == 0 || !name.isEmpty(), "File with empty name found under " + getName(parentId) + ", id=" + id);
1974
1975     checkContentsStorageSanity(id);
1976     checkAttributesStorageSanity(id, usedAttributeRecordIds, validAttributeIds);
1977
1978     long length = getLength(id);
1979     assert length >= -1 : "Invalid file length found for " + name + ": " + length;
1980   }
1981
1982   private static void checkContentsStorageSanity(int id) {
1983     int recordId = getContentRecordId(id);
1984     assert recordId >= 0;
1985     if (recordId > 0) {
1986       getContentStorage().checkSanity(recordId);
1987     }
1988   }
1989
1990   private static void checkAttributesStorageSanity(int id, IntArrayList usedAttributeRecordIds, IntArrayList validAttributeIds) {
1991     int attributeRecordId = getAttributeRecordId(id);
1992
1993     assert attributeRecordId >= 0;
1994     if (attributeRecordId > 0) {
1995       try {
1996         checkAttributesSanity(attributeRecordId, usedAttributeRecordIds, validAttributeIds);
1997       }
1998       catch (IOException ex) {
1999         DbConnection.handleError(ex);
2000       }
2001     }
2002   }
2003
2004   private static void checkAttributesSanity(final int attributeRecordId, final IntArrayList usedAttributeRecordIds,
2005                                             final IntArrayList validAttributeIds) throws IOException {
2006     assert !usedAttributeRecordIds.contains(attributeRecordId);
2007     usedAttributeRecordIds.add(attributeRecordId);
2008
2009     try (DataInputStream dataInputStream = getAttributesStorage().readStream(attributeRecordId)) {
2010       if (bulkAttrReadSupport) skipRecordHeader(dataInputStream, 0, 0);
2011
2012       while (dataInputStream.available() > 0) {
2013         int attId = DataInputOutputUtil.readINT(dataInputStream);
2014
2015         if (!validAttributeIds.contains(attId)) {
2016           assert persistentAttributesList || !getNames().valueOf(attId).isEmpty();
2017           validAttributeIds.add(attId);
2018         }
2019
2020         int attDataRecordIdOrSize = DataInputOutputUtil.readINT(dataInputStream);
2021
2022         if (inlineAttributes) {
2023           if (attDataRecordIdOrSize < MAX_SMALL_ATTR_SIZE) {
2024             dataInputStream.skipBytes(attDataRecordIdOrSize);
2025             continue;
2026           }
2027           else attDataRecordIdOrSize -= MAX_SMALL_ATTR_SIZE;
2028         }
2029         assert !usedAttributeRecordIds.contains(attDataRecordIdOrSize);
2030         usedAttributeRecordIds.add(attDataRecordIdOrSize);
2031
2032         getAttributesStorage().checkSanity(attDataRecordIdOrSize);
2033       }
2034     }
2035   }
2036
2037   public static void handleError(Throwable e) throws RuntimeException, Error {
2038     DbConnection.handleError(e);
2039   }
2040
2041   /*
2042   public interface BulkAttrReadCallback {
2043     boolean accepts(int fileId);
2044     boolean execute(int fileId, DataInputStream is);
2045   }
2046
2047   // custom DataInput implementation instead of DataInputStream (without extra allocations) (api change)
2048   // store each attr in separate file: pro: read only affected data, easy versioning
2049
2050   public static void readAttributeInBulk(FileAttribute attr, BulkAttrReadCallback callback) throws IOException {
2051     String attrId = attr.getId();
2052     int encodedAttrId = DbConnection.getAttributeId(attrId);
2053     synchronized (attrId) {
2054       Storage storage = getAttributesStorage();
2055       RecordIterator recordIterator = storage.recordIterator();
2056       while (recordIterator.hasNextRecordId()) {
2057         int recordId = recordIterator.nextRecordId();
2058         DataInputStream stream = storage.readStream(recordId);
2059
2060         int currentAttrId = DataInputOutputUtil.readINT(stream);
2061         int fileId = DataInputOutputUtil.readINT(stream);
2062         if (!callback.accepts(fileId)) continue;
2063
2064         if (currentAttrId == DbConnection.RESERVED_ATTR_ID) {
2065           if (!inlineAttributes) continue;
2066
2067           while(stream.available() > 0) {
2068             int directoryAttrId = DataInputOutputUtil.readINT(stream);
2069             int directoryAttrAddressOrSize = DataInputOutputUtil.readINT(stream);
2070
2071             if (directoryAttrId != encodedAttrId) {
2072               if (directoryAttrAddressOrSize < MAX_SMALL_ATTR_SIZE) stream.skipBytes(directoryAttrAddressOrSize);
2073             } else {
2074               if (directoryAttrAddressOrSize < MAX_SMALL_ATTR_SIZE) {
2075                 byte[] b = new byte[directoryAttrAddressOrSize];
2076                 stream.readFully(b);
2077                 DataInputStream inlineAttrStream = new DataInputStream(new ByteArrayInputStream(b));
2078                 int version = DataInputOutputUtil.readINT(inlineAttrStream);
2079                 if (version != attr.getVersion()) continue;
2080                 boolean result = callback.execute(fileId, inlineAttrStream); // todo
2081                 if (!result) break;
2082               }
2083             }
2084           }
2085         } else if (currentAttrId == encodedAttrId) {
2086           int version = DataInputOutputUtil.readINT(stream);
2087           if (version != attr.getVersion()) continue;
2088
2089           boolean result = callback.execute(fileId, stream); // todo
2090           if (!result) break;
2091         }
2092       }
2093     }
2094   }*/
2095 }