/*******************************************************************************
* Copyright (c) 2012, 2013 Pivotal Software, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal Software, Inc. - initial API and implementation
*******************************************************************************/
package org.springsource.ide.eclipse.commons.content.core;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.concurrent.CopyOnWriteArrayList;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.osgi.util.NLS;
import org.springsource.ide.eclipse.commons.content.core.util.Descriptor;
import org.springsource.ide.eclipse.commons.content.core.util.Descriptor.Dependency;
import org.springsource.ide.eclipse.commons.content.core.util.DescriptorReader;
import org.springsource.ide.eclipse.commons.content.core.util.IContentConstants;
import org.springsource.ide.eclipse.commons.core.HttpUtil;
import org.springsource.ide.eclipse.commons.core.ResourceProvider;
import org.springsource.ide.eclipse.commons.core.StatusHandler;
import org.springsource.ide.eclipse.commons.internal.content.core.DescriptorMatcher;
/**
* Manages the list of available tutorials, template and sample projects.
* Refresh requests only refresh descriptors for tutorials, templates and sample
* projects that are stored in a state file. The refresh operation reads changed
* descriptors and persists them in a state file, and then reinitialises the
* content item model with the changed content.
*
* <p/>
* In addition, it also supports descriptors specified through additional
* content locations (for example locations inside an Eclipse bundle), which are
* meant to point to descriptors that are not persisted in state files, and only
* loaded during the runtime session.
* @author Terry Denney
* @author Steffen Pingel
* @author Christian Dupuis
* @author Kris De Volder
* @author Kaitlin Duck Sherwood
*/
public class ContentManager {
private static final boolean DEBUG = ("" + Platform.getLocation()).contains("bamboo");
// lightweight lock
private boolean isRefreshing = false;
private boolean isDirty = false;
public boolean isDirty() {
return isDirty;
}
public void setDirty() {
isDirty = true;
}
public boolean isRefreshing() {
return isRefreshing;
}
private static void debug(String msg) {
if (DEBUG) {
System.out.println(msg);
}
}
private void debug(Exception e) {
if (DEBUG) {
e.printStackTrace(System.out);
}
}
public static final String EVENT_REFRESH = "refresh";
public static final String KIND_SAMPLE = "sample";
public static final String KIND_TEMPLATE = "template";
public static final String KIND_TUTORIAL = "tutorial";
private static final String DIRECTORY_METADATA = ".metadata";
private static final String DIRECTORY_STS = ".sts";
private static final String DIRECTORY_INSTALL = "content";
public static final String ARCHIVE_EXTENSION = ".zip";
public static final String[] DESCRIPTOR_FILENAMES = { IContentConstants.SAMPLE_PROJECT_DATA_FILE_NAME,
IContentConstants.TUTORIAL_DATA_FILE_NAME, IContentConstants.SERVER_DATA_FILE_NAME,
IContentConstants.TEMPLATE_DATA_FILE_NAME };
public static final String RESOURCE_CONTENT_DESCRIPTORS = "content.descriptors";
private final Map<String, ContentItem> itemById;
private final Map<String, Set<ContentItem>> itemsByKind;
private final List<PropertyChangeListener> listeners;
private File stateFile;
private File defaultStateFile;
public ContentManager() {
itemById = new HashMap<String, ContentItem>();
itemsByKind = new HashMap<String, Set<ContentItem>>();
listeners = new CopyOnWriteArrayList<PropertyChangeListener>();
isDirty = true;
}
public void addListener(PropertyChangeListener listener) {
listeners.add(listener);
}
public TemplateDownloader createDownloader(ContentItem item) {
return new TemplateDownloader(item);
}
private ContentItem createItem(Descriptor descriptor) {
String id = descriptor.getId();
ContentItem item = new ContentItem(id);
itemById.put(id, item);
String kind = descriptor.getKind();
Set<ContentItem> items = itemsByKind.get(kind);
if (items == null) {
items = new HashSet<ContentItem>();
itemsByKind.put(kind, items);
}
items.add(item);
return item;
}
private void firePropertyChangeEvent(String eventName) {
PropertyChangeListener[] listeners = this.listeners.toArray(new PropertyChangeListener[0]);
if (listeners.length > 0) {
PropertyChangeEvent event = new PropertyChangeEvent(this, eventName, null, null);
for (PropertyChangeListener listener : listeners) {
listener.propertyChange(event);
}
}
}
public File getDataDirectory() {
return new File(ResourcesPlugin.getWorkspace().getRoot().getLocation().append(DIRECTORY_METADATA)
.append(DIRECTORY_STS).toOSString());
}
public File getDefaultStateFile() {
return defaultStateFile;
}
/**
* @param item
* @return
* @throws CoreException
*/
public List<ContentItem> getDependencies(ContentItem item) throws CoreException {
List<ContentItem> results = new ArrayList<ContentItem>();
Stack<ContentItem> queue = new Stack<ContentItem>();
queue.add(item);
while (!queue.isEmpty()) {
ContentItem next = queue.pop();
results.add(next);
List<Dependency> dependencies = next.getRemoteDescriptor().getDependencies();
for (Dependency dependency : dependencies) {
ContentItem dependentItem = itemById.get(dependency.getId());
if (dependentItem == null) {
String message = NLS.bind(
"Failed to resolve dependencies: ''{0}'' requires ''{1}'' which is not available",
next.getId(), dependency.getId());
throw new CoreException(new Status(IStatus.ERROR, ContentPlugin.PLUGIN_ID, message));
}
if (dependentItem.needsDownload()) {
if (!results.contains(dependentItem)) {
queue.add(dependentItem);
}
}
}
}
return results;
}
public File getInstallDirectory() {
return new File(getDataDirectory(), DIRECTORY_INSTALL);
}
public File getInstallDirectory(ContentItem item) {
return new File(getInstallDirectory(), item.getPath());
}
public ContentItem getItem(String id) {
return itemById.get(id);
}
public Collection<ContentItem> getItems() {
return new HashSet<ContentItem>(itemById.values());
}
public Collection<ContentItem> getItemsByKind(String kind) {
Set<ContentItem> items = itemsByKind.get(kind);
if (items == null) {
items = new HashSet<ContentItem>();
}
return Collections.unmodifiableCollection(items);
}
private Set<String> getRemoteDescriptorLocations() {
return new HashSet<String>(Arrays.asList(ResourceProvider.getUrls(RESOURCE_CONTENT_DESCRIPTORS)));
}
public File getStateFile() {
return stateFile;
}
public void init() {
itemById.clear();
itemsByKind.clear();
MultiStatus result = new MultiStatus(ContentPlugin.PLUGIN_ID, 0, NLS.bind("Reading of content failed", null),
null);
File file = getStateFile();
if (file != null && file.exists()) {
try {
read(file);
}
catch (CoreException e) {
StatusHandler.log(new Status(IStatus.WARNING, ContentPlugin.PLUGIN_ID, NLS.bind(
"Detected error in ''{0}''", file.getAbsoluteFile()), e));
}
}
file = getDefaultStateFile();
if (file != null) {
try {
read(file);
}
catch (CoreException e) {
StatusHandler.log(new Status(IStatus.WARNING, ContentPlugin.PLUGIN_ID, NLS.bind(
"Detected error in ''{0}''", file.getAbsoluteFile()), e));
}
}
if (!result.isOK()) {
StatusHandler.log(result);
}
firePropertyChangeEvent(EVENT_REFRESH);
}
private void read(File file) throws CoreException {
DescriptorMatcher matcher = new DescriptorMatcher(this);
DescriptorReader reader = new DescriptorReader();
reader.read(file);
List<Descriptor> descriptors = reader.getDescriptors();
for (Descriptor descriptor : descriptors) {
if (!matcher.match(descriptor)) {
continue;
}
ContentItem item = itemById.get(descriptor.getId());
if (item == null) {
item = createItem(descriptor);
if (item != null) {
itemById.put(item.getId(), item);
}
}
if (item != null) {
if (descriptor.isLocal()) {
item.setLocalDescriptor(descriptor);
}
else {
item.setRemoteDescriptor(descriptor);
}
}
}
}
/**
* @param reader reads content from the specified location
* @param location an HTTP URL
* @param monitor
*
* @throws CoreException
*/
private void readFromUrl(DescriptorReader reader, String location, IProgressMonitor monitor) throws CoreException {
debug("entering readFromURL: " + location);
try {
URI uri = new URI(location);
InputStream in = HttpUtil.stream(uri, monitor);
try {
reader.read(in);
}
catch (Exception e) {
String message = NLS.bind("Error downloading {0} - Internet connection might be down", location);
throw new CoreException(new Status(IStatus.ERROR, ContentPlugin.PLUGIN_ID, message, e));
}
finally {
debug("exiting readFromURL: " + location);
try {
in.close();
}
catch (IOException e) {
String message = NLS.bind("No route to {0} - Internet connection might be down", location);
throw new CoreException(new Status(IStatus.ERROR, ContentPlugin.PLUGIN_ID, message, e));
}
}
}
catch (URISyntaxException e) {
debug(e);
String message = NLS.bind("I/O error while retrieving data: ", e);
throw new CoreException(new Status(IStatus.ERROR, ContentPlugin.PLUGIN_ID, message, e));
}
catch (CoreException e) {
String message = NLS.bind("Error while retrieving {0}", location);
throw new CoreException(new Status(IStatus.ERROR, ContentPlugin.PLUGIN_ID, message, e));
}
}
/**
* Refreshes the list of descriptors, and persists them in the state file.
* It then re-initialises all the content items.
* @param monitor
* @param shouldDownloadRemotes
* @param bundle optional. can be null
* @return
*/
public IStatus refresh(IProgressMonitor monitor, boolean shouldDownloadRemotes) {
File targetFile = getStateFile();
Assert.isNotNull(targetFile, "stateFile not initialized");
isRefreshing = true;
SubMonitor progress = SubMonitor.convert(monitor, 100);
try {
progress.beginTask("Refreshing", 200);
MultiStatus result = new MultiStatus(ContentPlugin.PLUGIN_ID, 0, "Results of template project refresh:",
null);
DescriptorReader reader = new DescriptorReader();
if (shouldDownloadRemotes) {
markLocalTemplatesAsLocal(progress, result, reader);
for (String descriptorLocation : getRemoteDescriptorLocations()) {
// remote descriptor
try {
if (descriptorLocation != null && descriptorLocation.length() > 0) {
readFromUrl(reader, descriptorLocation, progress.newChild(70));
}
}
catch (CoreException e) {
String message = NLS.bind(
"Error while downloading or parsing descriptors file ''{0}'':\n\n{1}",
descriptorLocation, e);
result.add(new Status(IStatus.ERROR, ContentPlugin.PLUGIN_ID, message, e));
}
}
}
else {
try {
reader.read(targetFile);
}
catch (CoreException e) {
String message = NLS.bind("Failed to store updated descriptors to ''{0}''",
targetFile.getAbsolutePath());
result.add(new Status(IStatus.ERROR, ContentPlugin.PLUGIN_ID, message, e));
}
markLocalTemplatesAsLocal(progress, result, reader);
}
// store on disk
try {
if (result.isOK()) {
reader.write(targetFile);
init();
}
}
catch (CoreException e) {
String message = NLS.bind("Failed to store updated descriptors to ''{0}''",
targetFile.getAbsolutePath());
result.add(new Status(IStatus.ERROR, ContentPlugin.PLUGIN_ID, message, e));
}
if (result.isOK()) {
isDirty = false;
}
return result;
}
finally {
isRefreshing = false;
progress.done();
}
}
// Right after a template project is downloaded, the ContentManager doesn't
// know yet that the project has been downloaded. The way that the
// ContentManager finds that out is by walking the installation directory
// and marking every project it finds as local.
// Note that the ContentManager determines if a download was successful by
// looking to see if that project is now marked as local.
public void markLocalTemplatesAsLocal(SubMonitor progress, MultiStatus result, DescriptorReader reader) {
IStatus descriptorParseStatus = new Status(IStatus.OK, ContentPlugin.PLUGIN_ID, NLS.bind(
"No descriptors were found.", null));
File dir = getInstallDirectory();
File[] children = dir.listFiles();
if (children == null || children.length <= 0) {
progress.setWorkRemaining(70);
return;
}
SubMonitor loopProgress = progress.newChild(30).setWorkRemaining(children.length);
for (File childDirectory : children) {
if (childDirectory.isDirectory()) {
if ((new File(childDirectory, IContentConstants.TEMPLATE_DATA_FILE_NAME).exists())) {
descriptorParseStatus = setDirectoryDescriptorsToLocal(reader, childDirectory);
}
else {
// Files downloaded directly from template.xml (as opposed
// to via descriptors.xml) can be in a subdirectory when
// they first get extracted. They will eventually get moved
// up one directory level, but that might not happen by the
// time we reach here.
File[] grandchildren = childDirectory.listFiles();
for (File grandchildDirectory : grandchildren) {
if (grandchildDirectory.isDirectory()) {
descriptorParseStatus = setDirectoryDescriptorsToLocal(reader, grandchildDirectory);
}
}
}
if (!descriptorParseStatus.isOK()) {
result.add(descriptorParseStatus);
}
}
loopProgress.worked(1);
}
}
public IStatus setDirectoryDescriptorsToLocal(DescriptorReader reader, File directory) {
boolean descriptorFound = false;
for (String filename : DESCRIPTOR_FILENAMES) {
File descriptorFile = new File(directory, filename);
if (descriptorFile.exists()) {
descriptorFound = true;
try {
List<Descriptor> localDescriptors = reader.read(descriptorFile);
for (Descriptor descriptor : localDescriptors) {
descriptor.setLocal(true);
}
}
catch (CoreException e) {
String message = NLS.bind("Error while parsing ''{0}''", descriptorFile.getAbsolutePath());
return new Status(IStatus.ERROR, ContentPlugin.PLUGIN_ID, message, e);
}
}
}
if (descriptorFound) {
return new Status(IStatus.OK, ContentPlugin.PLUGIN_ID, NLS.bind("Everything is okay", null));
}
else {
return new Status(IStatus.OK, ContentPlugin.PLUGIN_ID, NLS.bind(
"There are zero descriptors, but that is okay.", null));
}
}
public void removeListener(PropertyChangeListener listener) {
listeners.remove(listener);
}
public void setDefaultStateFile(File defaultStateFile) {
this.defaultStateFile = defaultStateFile;
}
public void setStateFile(File stateFile) {
this.stateFile = stateFile;
}
}