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