| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package org.netbeans; |
| |
| import java.io.DataOutputStream; |
| import java.io.IOException; |
| import java.io.UnsupportedEncodingException; |
| import java.nio.ByteBuffer; |
| import java.util.HashMap; |
| import java.util.LinkedHashMap; |
| import java.util.Map; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| |
| /** |
| * A shared startup-needed resource archive. |
| * File format of the archive: |
| * [Header] |
| * ([Source entry]|[File entry])* |
| * |
| * Header: |
| * 8B [Magic] |
| * 8B [timestamp] |
| * |
| * Source entry (describes a data source for following entries): |
| * 1B 0x01 type identifier |
| * xB id utf8 String identifier of the source (file name) |
| * |
| * File entry (keeps content of a file with name and source ref): |
| * 1B 0x02 type identifier (0x03 for general) |
| * 2B src number of the source (sources are counted in file from 0) |
| * 4B len length of the data (or -1 for no such file for source) |
| * xB name utf8 String name of the file |
| * lenB data file content |
| * |
| * Utf8 string |
| * 2B len length of the following String in bytes |
| * lenB data utf8 encoded string |
| * |
| * @author nenik |
| */ |
| class Archive implements Stamps.Updater { |
| // increment on format change |
| private static final long magic = 6836742066851800321l; |
| private static final Logger LOG = Logger.getLogger(Archive.class.getName()); |
| |
| private volatile boolean saved; |
| private final boolean prepopulated; |
| |
| private volatile boolean gathering; |
| private final Object gatheringLock = new Object(); |
| // these two collections are guarded either by the gatheringLock |
| // or by the "gathering" volatile flag transitions |
| private Map<String,Boolean> requests = new LinkedHashMap<String, Boolean>(); |
| private Map<String,ArchiveResources> knownSources = new HashMap<String,ArchiveResources>(); |
| |
| private volatile boolean active; |
| // These two collections are guarded by the "active" volatile flag transition. |
| // They are modified from a single thread only, when "active" flag is false |
| private Map<String,Integer> sources = new HashMap<String,Integer>(); |
| private Map<Entry, Entry> entries = new HashMap<Entry,Entry>(); |
| |
| public Archive() { |
| gathering = false; |
| active = false; |
| prepopulated = false; |
| } |
| |
| Archive(boolean prep) { |
| gathering = false; |
| active = false; |
| prepopulated = prep; |
| } |
| |
| /** Creates a new instance of Archive that reads data from given cache |
| */ |
| Archive(Stamps cache) { |
| ByteBuffer master = cache.asByteBuffer("all-resources.dat"); |
| if (master != null) { |
| try { |
| parse(master, cache.lastModified()); |
| } catch (Exception e) { |
| sources.clear(); |
| entries.clear(); |
| } |
| } else { |
| sources.clear(); |
| entries.clear(); |
| } |
| prepopulated = entries.size() > 0; |
| |
| active = true; |
| gathering = true; |
| } |
| |
| final boolean isActive() { |
| return active; |
| } |
| |
| /** |
| * Sweep through the master buffer and remember all the entries |
| */ |
| private void parse(ByteBuffer master, long after) throws Exception { |
| if (master.remaining() < 16) throw new IllegalStateException("Cache invalid"); |
| if (master.getLong() != magic) throw new IllegalStateException("Wrong format"); |
| if (master.getLong() < after) throw new IllegalStateException("Cache outdated"); |
| |
| int srcCounter = 0; |
| |
| while (master.remaining() > 0) { |
| int type = master.get(); |
| switch (type) { |
| case 1: // source header |
| String name = parseString(master); |
| sources.put(name, srcCounter++); |
| break; |
| case 2: |
| Entry en = new Entry(master); // shifts the buffer |
| entries.put(en, en); |
| break; |
| default: |
| throw new IllegalStateException("Cache invalid"); |
| } |
| } |
| master.rewind(); |
| } |
| |
| private static String parseString(ByteBuffer src) { |
| int len = src.getChar(); |
| byte data[] = new byte[len]; |
| src.get(data); |
| try { |
| return new String(data, "UTF8"); |
| } catch (UnsupportedEncodingException uee) { |
| throw new InternalError(); // UTF8 must be supported |
| } |
| } |
| |
| private static void writeString(DataOutputStream dos, String str) throws UnsupportedEncodingException, IOException { |
| byte[] data = str.getBytes("UTF8"); |
| dos.writeChar(data.length); |
| dos.write(data); |
| } |
| |
| @SuppressWarnings("element-type-mismatch") |
| public byte[] getData(ArchiveResources source, String name) throws IOException { |
| Entry e = null; |
| String srcId = source.getIdentifier(); |
| Map<Entry, Entry> ents = entries; |
| if (active) { |
| Integer src = sources.get(srcId); |
| if (src == null) { |
| e = null; |
| } else { |
| e = ents.get(new Template(src, name)); // or null |
| } |
| if (e == null && gathering) { |
| StringBuilder sb = new StringBuilder(srcId.length() + name.length()); |
| String key = sb.append(srcId).append(name).toString(); |
| |
| synchronized(gatheringLock) { |
| if (gathering) { |
| if (!knownSources.containsKey(srcId)) knownSources.put(srcId, source); |
| if (!requests.containsKey(key)) requests.put(key, Boolean.TRUE); |
| } |
| } |
| } |
| } |
| if (e == null) { |
| byte[] data = source.resource(name); |
| // maybe store it now? No. |
| return data; |
| } |
| |
| return e.getContent(); |
| } |
| |
| public void stopGathering() { |
| synchronized (gatheringLock) { |
| gathering = false; |
| } |
| } |
| |
| public void stopServing() { |
| active = false; |
| // thread-safe, the only place using the field after |
| // construction is guarded by above-cleared volatile flag |
| // and this free happens-after clearing the flag |
| entries = null; |
| } |
| |
| public void save(Stamps cache) throws IOException { |
| if (saved) { |
| return; |
| } |
| saved = true; |
| cache.scheduleSave(this, "all-resources.dat", prepopulated); |
| } |
| |
| @Override |
| public void flushCaches(DataOutputStream dos) throws IOException { |
| stopGathering(); |
| stopServing(); |
| |
| assert !gathering; |
| assert !active; |
| |
| if (!prepopulated) { // write header |
| dos.writeLong(magic); |
| dos.writeLong(System.currentTimeMillis()); |
| } |
| |
| // no need to really synchronize on this collection, gathering flag |
| // is already cleared |
| for (String s:requests.keySet()) { |
| String[] parts = s.split("(?<=!/)", 2); |
| String name = parts.length == 2 ? parts[1] : ""; |
| ArchiveResources src = knownSources.get(parts[0]); |
| assert src != null : "Could not find " + s + " in " + knownSources; |
| byte[] data = src.resource(name); |
| Integer srcId = sources.get(parts[0]); |
| if (srcId == null) { |
| srcId = sources.size(); |
| sources.put(parts[0], srcId); |
| dos.write(1); |
| writeString(dos, parts[0]); |
| } |
| |
| dos.write(2); |
| dos.writeChar(srcId); |
| dos.writeInt(data == null ? -1 : data.length); // store a marker to avoid openning |
| writeString(dos, name); |
| if (data != null) { |
| dos.write(data); |
| } |
| } |
| dos.close(); |
| |
| if (LOG.isLoggable(Level.FINER)) { |
| for (Object r : requests.keySet()) { |
| LOG.log(Level.FINER, "archiving: {0}", r); |
| } |
| } |
| |
| // clean up |
| requests = null; |
| knownSources = null; |
| sources = null; |
| } |
| |
| @Override |
| public void cacheReady() { |
| // nothing needs to be done |
| } |
| |
| final boolean isPopulated() { |
| return prepopulated; |
| } |
| |
| /* Entry layout in the buffer: |
| * -1 1B 0x02 type identifier (0x03 for general) |
| * 0 -> 2B src number of the source (sources are counted in file from 0) |
| * 2 4B len length of the data |
| * 6 2B x Length of name (in bytes) |
| * 8 xB name utf8 String name of the file |
| *x+8 yB data file content |
| */ |
| |
| private static class Entry { |
| private final int offset; |
| private final ByteBuffer master; |
| |
| Entry(ByteBuffer m) { |
| master = m; |
| offset = master.position(); |
| int fLen = master.getInt(offset+2); |
| int nLen = master.getChar(offset+6); |
| if (fLen < 0) fLen = 0; |
| master.position(offset+8+nLen+fLen); |
| } |
| |
| String getName() { |
| ByteBuffer my = master.duplicate(); |
| my.position(offset+6); |
| return parseString(my); |
| } |
| |
| int getSource() { |
| return master.getChar(offset); |
| } |
| |
| byte[] getContent() { |
| int fLen = master.getInt(offset+2); |
| int nLen = master.getChar(offset+6); |
| if (fLen < 0) return null; |
| |
| ByteBuffer clone = master.duplicate(); |
| clone.position(offset+8+nLen); |
| byte[] content = new byte[fLen]; |
| clone.get(content); |
| return content; |
| } |
| |
| public @Override int hashCode() { |
| ByteBuffer clone = master.duplicate(); |
| clone.position(offset+8); |
| clone.limit(offset+8+master.getChar(offset+6)); |
| |
| int code = 53*master.getChar(offset); |
| while (clone.hasRemaining()) code = code*53 + clone.get(); |
| return code; |
| } |
| |
| public @Override boolean equals(Object obj) { |
| if (obj instanceof Template) return obj.equals(this); |
| return obj == this; |
| } |
| |
| public @Override String toString() { |
| return "#" + getSource() + ":" + getName() + "=[" + offset + "]"; |
| } |
| } |
| |
| // template |
| private static class Template { |
| private int source; |
| private byte[] utf; |
| |
| Template(int src, String name) { |
| try { |
| this.source = src; |
| utf = name.getBytes("UTF8"); |
| } catch (UnsupportedEncodingException ex) { |
| throw new InternalError(); |
| } |
| } |
| |
| public @Override boolean equals(Object o) { |
| if (! (o instanceof Entry)) return false; |
| Entry e = (Entry)o; |
| |
| if (source != e.master.getChar(e.offset)) return false; |
| if (utf.length != e.master.getChar(e.offset+6)) return false; |
| |
| ByteBuffer clone = e.master.duplicate(); |
| clone.position(((Entry)o).offset+8); |
| |
| |
| for (byte b : utf) if (b != clone.get()) return false; |
| |
| return true; |
| } |
| |
| public @Override int hashCode() { |
| int code = 53*source; |
| for (byte b : utf) code = code*53 + b; |
| return code; |
| } |
| } |
| } |