/* * Copyright 2008 Google Inc. * * Licensed 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 com.google.gwt.dev.resource.impl; import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.dev.util.collect.IdentityHashMap; import com.google.gwt.dev.util.collect.IdentityHashSet; import com.google.gwt.dev.util.collect.IdentityMaps; import com.google.gwt.dev.util.collect.Sets; import com.google.gwt.dev.util.msg.Message1String; import org.apache.commons.collections.map.AbstractReferenceMap; import org.apache.commons.collections.map.ReferenceIdentityMap; import org.apache.commons.collections.map.ReferenceMap; import java.io.File; import java.io.IOException; import java.util.Enumeration; import java.util.Map; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; /** * A classpath entry that is a jar or zip file. */ public class ZipFileClassPathEntry extends ClassPathEntry { /** * Logger messages related to this class. */ private static class Messages { static final Message1String BUILDING_INDEX = new Message1String( TreeLogger.TRACE, "Indexing zip file: $0"); static final Message1String EXCLUDING_RESOURCE = new Message1String( TreeLogger.DEBUG, "Excluding $0"); static final Message1String FINDING_INCLUDED_RESOURCES = new Message1String( TreeLogger.DEBUG, "Searching for included resources in $0"); static final Message1String INCLUDING_RESOURCE = new Message1String( TreeLogger.DEBUG, "Including $0"); static final Message1String READ_ZIP_ENTRY = new Message1String( TreeLogger.DEBUG, "$0"); } private static class ZipFileSnapshot { private final Map<AbstractResource, PathPrefix> cachedAnswers; private final int prefixSetSize; ZipFileSnapshot(int prefixSetSize, Map<AbstractResource, PathPrefix> cachedAnswers) { this.prefixSetSize = prefixSetSize; this.cachedAnswers = cachedAnswers; } } /** * Memory-sensitive cache of indexed {@link ZipFileClassPathEntry}s. URI of file is most probably * not referenced anywhere else, so we use hard reference, and soft reference on * {@link ZipFileClassPathEntry} allows its clearing in response to memory demand. */ @SuppressWarnings("unchecked") private static final Map<String, ZipFileClassPathEntry> entryCache = new ReferenceMap( AbstractReferenceMap.HARD, AbstractReferenceMap.SOFT); public static void clearCache() { entryCache.clear(); } /** * @return the {@link ZipFileClassPathEntry} instance for given jar or zip * file, may be shared with other users. */ public static synchronized ZipFileClassPathEntry get(File zipFile) throws IOException { String location = zipFile.toURI().toString(); ZipFileClassPathEntry entry = entryCache.get(location); if (entry == null) { entry = new ZipFileClassPathEntry(zipFile); entryCache.put(location, entry); } return entry; } private Set<ZipFileResource> allZipFileResources; /** * The lifetime of the {@link PathPrefixSet} pins the life time of the associated * {@link ZipFileSnapshot}; this is because the {@link PathPrefixSet} is referenced from module, * and {@link ZipFileSnapshot} is not referenced anywhere outside of {@link ZipFileClassPathEntry} * . When the module dies, the {@link ZipFileSnapshot} needs to die also. */ @SuppressWarnings("unchecked") private final Map<PathPrefixSet, ZipFileSnapshot> cachedSnapshots = new ReferenceIdentityMap( AbstractReferenceMap.WEAK, AbstractReferenceMap.HARD, true); private final long lastModified; private final String location; private final ZipFile zipFile; private ZipFileClassPathEntry(File zipFile) throws IOException { assert zipFile.isAbsolute(); this.lastModified = zipFile.lastModified(); this.zipFile = new ZipFile(zipFile); this.location = zipFile.toURI().toString(); } /** * Indexes the zip file on-demand, and only once over the life of the process. */ @Override public synchronized Map<AbstractResource, PathPrefix> findApplicableResources( TreeLogger logger, PathPrefixSet pathPrefixSet) { index(logger); ZipFileSnapshot snapshot = cachedSnapshots.get(pathPrefixSet); if (snapshot == null || snapshot.prefixSetSize != pathPrefixSet.getSize()) { snapshot = new ZipFileSnapshot(pathPrefixSet.getSize(), computeApplicableResources(logger, pathPrefixSet)); cachedSnapshots.put(pathPrefixSet, snapshot); } return snapshot.cachedAnswers; } @Override public String getLocation() { return location; } public ZipFile getZipFile() { return zipFile; } public long lastModified() { return lastModified; } synchronized void index(TreeLogger logger) { // Never re-index. if (allZipFileResources == null) { allZipFileResources = buildIndex(logger); } } private Set<ZipFileResource> buildIndex(TreeLogger logger) { logger = Messages.BUILDING_INDEX.branch(logger, zipFile.getName(), null); Set<ZipFileResource> results = new IdentityHashSet<ZipFileResource>(); Enumeration<? extends ZipEntry> e = zipFile.entries(); while (e.hasMoreElements()) { ZipEntry zipEntry = e.nextElement(); if (zipEntry.isDirectory()) { // Skip directories. continue; } if (zipEntry.getName().startsWith("META-INF/")) { // Skip META-INF since classloaders normally make this invisible. continue; } ZipFileResource zipResource = new ZipFileResource(this, zipEntry.getName()); results.add(zipResource); Messages.READ_ZIP_ENTRY.log(logger, zipEntry.getName(), null); } return Sets.normalize(results); } private Map<AbstractResource, PathPrefix> computeApplicableResources( TreeLogger logger, PathPrefixSet pathPrefixSet) { logger = Messages.FINDING_INCLUDED_RESOURCES.branch(logger, zipFile.getName(), null); Map<AbstractResource, PathPrefix> results = new IdentityHashMap<AbstractResource, PathPrefix>(); for (ZipFileResource r : allZipFileResources) { String path = r.getPath(); String[] pathParts = r.getPathParts(); PathPrefix prefix = null; if ((prefix = pathPrefixSet.includesResource(path, pathParts)) != null) { Messages.INCLUDING_RESOURCE.log(logger, path, null); results.put(r, prefix); } else { Messages.EXCLUDING_RESOURCE.log(logger, path, null); } } return IdentityMaps.normalize(results); } }