/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2010 Oracle and/or its affiliates. All rights reserved.
*
* Oracle and Java are registered trademarks of Oracle and/or its affiliates.
* Other names may be trademarks of their respective owners.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common
* Development and Distribution License("CDDL") (collectively, the
* "License"). You may not use this file except in compliance with the
* License. You can obtain a copy of the License at
* http://www.netbeans.org/cddl-gplv2.html
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
* specific language governing permissions and limitations under the
* License. When distributing the software, include this License Header
* Notice in each file and include the License file at
* nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the GPL Version 2 section of the License file that
* accompanied this code. If applicable, add the following below the
* License Header, with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* If you wish your version of this file to be governed by only the CDDL
* or only the GPL Version 2, indicate your decision by adding
* "[Contributor] elects to include this software in this distribution
* under the [CDDL or GPL Version 2] license." If you do not indicate a
* single choice of license, a recipient has the option to distribute
* your version of this file under either the CDDL, the GPL Version 2 or
* to extend the choice of license to its licensees as provided above.
* However, if you add GPL Version 2 code and therefore, elected the GPL
* Version 2 license, then the option applies only if the new code is
* made subject to such option by the copyright holder.
*
* Contributor(s):
*
* Portions Copyrighted 2009 Sun Microsystems, Inc.
*/
package org.netbeans.modules.ruby.rubyproject;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.netbeans.api.project.Project;
import org.netbeans.modules.ruby.platform.gems.Gem;
import org.netbeans.modules.ruby.platform.gems.GemFilesParser;
import org.netbeans.modules.ruby.platform.gems.Gems;
import org.netbeans.modules.ruby.rubyproject.GemRequirement.Status;
import org.openide.util.Parameters;
/**
* Helper class for dealing with the explicit gem requirements of
* a Ruby or Rails application. Contains info on the gem requirements, i.e. the gems
* and their versions that the application requires, and the gems and their versions
* that have actually been indexed.
*
* @author Erno Mononen
*/
public final class RequiredGems {
/**
* The project property for required gems.
*/
public static final String REQUIRED_GEMS_PROPERTY = "required.gems"; //NOI18N
/**
* The project property for the gems required in tests.
*/
public static final String REQUIRED_GEMS_TESTS_PROPERTY = "required.gems.tests"; //NOI18N
/** @GuardedBy("this") */
private List<GemRequirement> requirements;
/** @GuardedBy("this") */
private final List<URL> indexedGems = new ArrayList<URL>();
private final boolean forTests;
private RequiredGems(boolean forTests) {
this.forTests = forTests;
}
public static RequiredGems create(RubyBaseProject project) {
RequiredGems result = new RequiredGems(false);
result.setRequiredGems(fromString(project.evaluator().getProperty(REQUIRED_GEMS_PROPERTY)));
return result;
}
public static RequiredGems createForTests(RubyBaseProject project) {
RequiredGems result = new RequiredGems(true);
result.setRequiredGems(fromString(project.evaluator().getProperty(REQUIRED_GEMS_TESTS_PROPERTY)));
return result;
}
/**
* Looks up <code>RequiredGems</code> from the given <code>project</code>.
*
* @param project
* @return an array containing <code>RequiredGems</code>; <code>[0]</code> for sources and
* <code>[1]</code> for tests.
*/
public static RequiredGems[] lookup(Project project) {
Collection<? extends RequiredGems> reqGems = project.getLookup().lookupAll(RequiredGems.class);
assert reqGems.size() == 2;
RequiredGems rg = null;
RequiredGems rgTest = null;
for (RequiredGems each : reqGems) {
if (each.isForTests()) {
rgTest = each;
} else {
rg = each;
}
}
return new RequiredGems[]{rg, rgTest};
}
/**
* @return true if this represents required gems for tests.
*/
public boolean isForTests() {
return forTests;
}
/**
* Gets the gem requirements or <code>null</code> if no requirements
* have been explicitly set.
* @return
*/
public synchronized List<GemRequirement> getGemRequirements() {
if (requirements == null) {
return null;
}
List<GemRequirement> result = mergeVersions(requirements);
Collections.sort(result);
return result;
}
/**
* Adds the given requirements.
* @param gemRequirements
*/
public void addRequirements(Collection<GemRequirement> gemRequirements) {
Parameters.notNull("gemRequirements", gemRequirements);
synchronized (this) {
if (requirements == null) {
requirements = new ArrayList<GemRequirement>();
for (GemIndexingStatus status : getGemIndexingStatuses()) {
requirements.add(status.getRequirement());
}
}
for (GemRequirement each : gemRequirements) {
if (!requirements.contains(each)) {
requirements.add(each);
}
}
}
}
public static String asString(List<GemRequirement> requirements) {
if (requirements == null || requirements.isEmpty()) {
return "";
}
StringBuilder result = new StringBuilder();
for (Iterator<GemRequirement> it = requirements.iterator(); it.hasNext();) {
GemRequirement gemRequirement = it.next();
result.append(gemRequirement.asString());
if (it.hasNext()) {
result.append(", ");
}
}
return result.toString();
}
/**
* Sets the required gems. If <code>requirements</code> is <code>null</code>,
* clears the list of required gems.
*
* @param requirements
*/
public synchronized void setRequiredGems(List<GemRequirement> requirements) {
if (requirements == null) {
this.requirements = null;
} else {
this.requirements = new ArrayList<GemRequirement>(requirements);
}
}
/**
* Sets the required gems. If <code>requirements</code> is <code>null</code>,
* clears the list of required gems.
*
* @param requirements a comma separated list of the requirements.
*/
public void setRequiredGems(String requirements) {
setRequiredGems(fromString(requirements));
}
public synchronized List<URL> getIndexedGems() {
return Collections.unmodifiableList(indexedGems);
}
public synchronized void setIndexedGems(Collection<URL> gemUrls) {
Parameters.notNull("gemUrls", gemUrls);
indexedGems.clear();
indexedGems.addAll(gemUrls);
}
/**
* Filters out the gems that are not required from the given <code>gemUrls</code>.
*
* @param gemUrls
* @return the filtered collection.
*/
public synchronized Collection<URL> filterNotRequiredGems(Collection<URL> gemUrls) {
if (requirements == null && forTests) {
return gemUrls;
}
List<URL> result = new ArrayList<URL>();
for (URL url : gemUrls) {
String[] nameAndVersion = GemFilesParser.parseNameAndVersion(url);
if (nameAndVersion != null) {
String name = nameAndVersion[0];
String version = nameAndVersion[1];
// special cases, rails and rake (which are not listed by rake gems)
if (isRailsOrRake(name)) { //NOI18N
result.add(url);
continue;
}
// filter out testing gems if no requirements are specified
if (requirements == null && !forTests) {
// by default exclude testing gems
if (!Gems.isTestingGem(name)) {
result.add(url);
}
continue;
}
for (GemRequirement each : requirements) {
if (each.getName().equals(name) && each.satisfiedBy(version)) {
result.add(url);
break;
}
}
}
}
return result;
}
public synchronized List<GemIndexingStatus> getGemIndexingStatuses() {
// if there are no requirements, just add all the indexed gems
// this will also init requirements
boolean addAll = requirements == null;
// copy since we'll be removing elements
List<GemRequirement> requirementsCopy = new ArrayList<GemRequirement>();
if (requirements != null) {
requirementsCopy.addAll(requirements);
}
List<GemIndexingStatus> result = new ArrayList<GemIndexingStatus>();
for (URL gemUrl : indexedGems) {
String[] nameAndVersion = GemFilesParser.parseNameAndVersion(gemUrl);
if (nameAndVersion == null) {
// a warning msg already logged by GemFilesParser
continue;
}
boolean added = false;
String name = nameAndVersion[0];
String version = nameAndVersion[1];
if (addAll) { //NOI18N
result.add(new GemIndexingStatus(new GemRequirement(name,
null, null, Status.INSTALLED), version));
added = true;
} else {
for (Iterator<GemRequirement> it = requirementsCopy.iterator(); it.hasNext();) {
GemRequirement req = it.next();
if (req.getName().equals(name)) {
result.add(new GemIndexingStatus(req, version));
it.remove();
added = true;
break;
}
}
}
// add indexed gems that didn't have a corresponding requirement
if (!added) {
result.add(new GemIndexingStatus(new GemRequirement(name, null, null, Status.NOT_INSTALLED), version));
}
}
// add in reqs that didn't have a corresponding installed gem
if (!addAll) {
for (GemRequirement req : requirementsCopy) {
result.add(new GemIndexingStatus(req, null));
}
}
// add in gems that were indexed but don't have a corresponding req (typically rails gems)
Collections.sort(result, new Comparator<GemIndexingStatus>() {
public int compare(GemIndexingStatus o1, GemIndexingStatus o2) {
return o1.getRequirement().compareTo(o2.getRequirement());
}
});
return result;
}
/**
* Removes the requirement identified by the given <code>name</code>.
*
* @param name the name of requirement to remove.
*/
public void removeRequirement(String name) {
List<GemIndexingStatus> statuses = new ArrayList<GemIndexingStatus>(getGemIndexingStatuses());
synchronized(this) {
// can't just remove from requirements as it might not be set at all yet
if (this.requirements == null) {
List<GemRequirement> newReqs = new ArrayList<GemRequirement>();
for (GemIndexingStatus each : statuses) {
if (!each.getRequirement().getName().equals(name)) {
newReqs.add(each.getRequirement());
}
}
if (newReqs.size() < statuses.size()) {
setRequiredGems(newReqs);
}
} else {
for (Iterator<GemRequirement> it = requirements.iterator(); it.hasNext();) {
GemRequirement each = it.next();
if (each.getName().equals(name)) {
it.remove();
}
}
}
}
}
static List<GemRequirement> fromString(String str) {
if (str == null) {
return null;
}
String[] gems = str.split(",");
List<GemRequirement> result = new ArrayList<GemRequirement>();
for (String gem : gems) {
GemRequirement requirement = GemRequirement.fromString(gem.trim());
if (!result.contains(requirement)) {
result.add(requirement);
}
}
return result;
}
private static boolean isRailsOrRake(String name) {
return Gems.isRailsGem(name) || Gems.isRakeGem(name);
}
private static List<GemRequirement> mergeVersions(List<GemRequirement> requirements) {
// XXX: performs a very basic version comparison; doesn't take into account the operator etc.
Map<String, GemRequirement> map = new HashMap<String, GemRequirement>();
for (GemRequirement each : requirements) {
GemRequirement existing = map.get(each.getName());
if (existing != null) {
if (existing.compareTo(each) < 0) {
map.put(each.getName(), each);
}
} else {
map.put(each.getName(), each);
}
}
return new ArrayList<GemRequirement>(map.values());
}
public static final class GemIndexingStatus {
private final GemRequirement requirement;
private final String indexedVersion;
private GemIndexingStatus(GemRequirement requirement, String indexedVersion) {
this.requirement = requirement;
this.indexedVersion = indexedVersion;
}
public String getIndexedVersion() {
return indexedVersion;
}
public GemRequirement getRequirement() {
return requirement;
}
}
}