/*******************************************************************************
* Copyright (c) 2015 IBM Corp.
*
* 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.ibm.ws.repository.strategies.writeable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import com.ibm.ws.repository.common.enums.DisplayPolicy;
import com.ibm.ws.repository.common.enums.FilterPredicate;
import com.ibm.ws.repository.common.enums.FilterableAttribute;
import com.ibm.ws.repository.common.enums.ResourceType;
import com.ibm.ws.repository.common.enums.State;
import com.ibm.ws.repository.common.enums.Visibility;
import com.ibm.ws.repository.connections.RepositoryConnection;
import com.ibm.ws.repository.exceptions.RepositoryBackendException;
import com.ibm.ws.repository.exceptions.RepositoryIllegalArgumentException;
import com.ibm.ws.repository.exceptions.RepositoryResourceException;
import com.ibm.ws.repository.exceptions.RepositoryResourceValidationException;
import com.ibm.ws.repository.resources.ApplicableToProduct;
import com.ibm.ws.repository.resources.RepositoryResource;
import com.ibm.ws.repository.resources.internal.AppliesToProcessor;
import com.ibm.ws.repository.resources.internal.EsaResourceImpl;
import com.ibm.ws.repository.resources.internal.RepositoryResourceImpl;
import com.ibm.ws.repository.resources.writeable.ProductResourceWritable;
import com.ibm.ws.repository.resources.writeable.RepositoryResourceWritable;
import com.ibm.ws.repository.resources.writeable.WebDisplayable;
import com.ibm.ws.repository.transport.model.AppliesToFilterInfo;
/**
* This class is used to ensure that when we add an asset only one asset remains visible on the website.
* <p>
* This is controlled by two concepts:
* <ul>
* <li>Matching - a matching resource refers to the same item. Only one of these should ever be in the repository, hidden or not.
* <li>VanityURL - an item who's vanity URL is the same (but does not match) is a different version of the same resource.
* </ul>
* When we add an item with the same VanityURL we decide which is 'newer' (see below) and only the newer item should be visible.
* Beta items are always hidden by non-beta items.
* <p>
* Determining which item is newer:
* <ul>
* <li> For Products
* <ul>
* <li> The product version is used to determine which product is newer
* </ul>
* <li> For non-Products
* <ul>
* <li> The appliesTo field is used to determine which resource is newer. The resource that applies to the highest
* MINIMUM version is the newest. e.g. "8.5.5.5+" is is older than "8.5.5.6" even though "8.5.5.5+" would apply to "8.5.5.7".
* <li> If we don't have a product version from the appliesTo information (or we have a tie) we look at the version field.
* For this to work the resource version field needs to be an OSGI version e.g. "1.0.0" rather than "Version 1".
* </ul>
* </ul>
*/
public class AddThenHideOldStrategy extends AddThenDeleteStrategy {
private static final Version4Digit MAX_VERSION = new Version4Digit(Integer.MAX_VALUE, 0, 0, "0");
private static final Version4Digit MIN_VERSION = new Version4Digit(0, 0, 0, "0");
/**
* Delegate to super class for states
*/
public AddThenHideOldStrategy() {}
/**
* Sets the desired state of the asset after uploading it
*
* @param desiredStateIfMatchingFound This is not used by this strategy but can be used by derived strategies
* @param desiredStateIfNoMatchingFound Set the resource to this state after uploading. This behaviour can
* be changed by derived classes
*/
public AddThenHideOldStrategy(State desiredStateIfMatchingFound, State desiredStateIfNoMatchingFound) {
super(desiredStateIfMatchingFound, desiredStateIfNoMatchingFound, false);
}
/** {@inheritDoc} */
@Override
public void uploadAsset(RepositoryResourceImpl newResource, List<RepositoryResourceImpl> matchingResources) throws RepositoryBackendException, RepositoryResourceException {
// we haven't added the newResource yet so blank its id in case we have stale data
newResource.resetId();
State desired;
if (matchingResources.size() != 0) {
desired = calculateTargetState(matchingResources.get(0));
} else {
desired = _desiredStateIfNoMatchingFound;
}
boolean performHideOnOldResource = false;
List<RepositoryResource> resourcesToHide = new ArrayList<RepositoryResource>();
// Lock on the vanityURL
String lockString = getVanityUrlLock(newResource.getVanityURL());
synchronized (lockString) {
// If the desired state is not published we will never hide any resources
if (desired == State.PUBLISHED) {
// If we adding a public beta feature make it visible (it will be hidden later
// if there is a non-beta version before being uploaded)
if (newResource instanceof EsaResourceImpl) {
EsaResourceImpl esa = (EsaResourceImpl) newResource;
if (isBeta(newResource) && Visibility.PUBLIC.equals(esa.getVisibility())) {
esa.setWebDisplayPolicy(DisplayPolicy.VISIBLE);
}
}
// get the resource(s) to hide (which may be the one we are adding)
resourcesToHide = findResourcesToHide(newResource, matchingResources);
// if the resource to hide is the resource being added make it hidden before upload.
// If it is another resource set a flag so that it gets uploaded later as hidden.
if (resourcesToHide.size() != 0) {
for (RepositoryResource loopResource : resourcesToHide) {
if (loopResource.getId() == null) {
// this is the resource we are adding (as it doesn't yet have an id) and we want it hidden
if (loopResource instanceof WebDisplayable) {
((WebDisplayable) loopResource).setWebDisplayPolicy(DisplayPolicy.HIDDEN);
}
} else {
// there are resources other than the one we are adding to hide
performHideOnOldResource = true;
}
}
}
} // end if (desired == State.PUBLISHED)
// upload the new resource (could be hidden or visible) ...
super.uploadAsset(newResource, matchingResources);
String newlyAddedAssetId = newResource.getId();
// now hide any OTHER assets that need hiding as THIS one will have been hidden if necessary before upload.
if (performHideOnOldResource) {
for (RepositoryResource resourceToHide : resourcesToHide) {
if ((resourceToHide.getId()).equals(newlyAddedAssetId)) {
// leave the asset we have just added alone
} else {
// this is not the recently added resource so hide
if (resourceToHide instanceof WebDisplayable) {
((WebDisplayable) resourceToHide).setWebDisplayPolicy(DisplayPolicy.HIDDEN);
RepositoryResourceWritable rrw = (RepositoryResourceWritable) resourceToHide;
// The desired stated is passed explicitly here, just in case a previous, failed,
// upload attempt has left a matching resource in the wrong state. There should
// always be a matching resource, so the desiredStateIfNoMatchingFound (second parameter)
// should never be used.
rrw.uploadToMassive(new AddThenDeleteStrategy(rrw.getState(), State.DRAFT, true));
}
}
}
}
}
}
/**
* This method should return a resource for the caller to hide if it matches the new resource being added. this resource:
* - has the same vanity URL as newResource
* - is visible
* - is published
* - is not the resource that is or appliesTo a higher version
* - when appliesTo versions are equal it returns one with the lower version (resource type dependent)
*
* @throws RepositoryResourceException
* @throws RepositoryBackendException
*/
private List<RepositoryResource> findResourcesToHide(RepositoryResourceImpl newResource, List<RepositoryResourceImpl> matchingResources)
throws RepositoryBackendException, RepositoryResourceException {
// build a list of matching ids
List<String> matchingResourceIds = new ArrayList<String>();
for (RepositoryResourceImpl r : matchingResources) {
matchingResourceIds.add(r.getId());
}
String vanityURL = newResource.getVanityURL();
RepositoryConnection repo = newResource.getRepositoryConnection();
Collection<RepositoryResource> resourcesWithSameVanityURLs =
repo.getMatchingResources(FilterPredicate.areEqual(FilterableAttribute.VANITY_URL, vanityURL));
List<RepositoryResource> resourcesToHide = new ArrayList<RepositoryResource>();
RepositoryResource newestResource = newResource;
// create list which excludes hidden and unpublished resources. Also exclude matching resources as they will
// be deleted so will not have to be hidden
for (RepositoryResource resource : resourcesWithSameVanityURLs) {
if (!isVisibleAndWebDisplayable(resource)) {
continue;
}
if (!State.PUBLISHED.equals(((RepositoryResourceWritable) resource).getState())) {
continue;
}
if (matchingResourceIds.contains(resource.getId())) {
continue;
}
// newestResource is passed to method as parameter #1 as this the default return value
// if there is no appliesTo / ProductVersion ie if we can't find which is the newer version we
// default to leaving the most recently added visible.
newestResource = getNewerResource(newestResource, resource);
resourcesToHide.add(resource);
}
// remove the highest matching asset from the list so it won't get hidden
// should only be one matching non-hidden resource.
resourcesToHide.remove(newestResource);
// add the newResource to the list if it wasn't the highest (ie eligible for hiding)
if (newestResource != newResource) {
resourcesToHide.add(newResource);
}
return resourcesToHide;
}
/**
* Return the higher version of the resource or the first one if this cannot be determined. For Products
* this is based off the ProductVersion and for everything else it is based of the appliesTo information.
*
* For resources that have the same appliesTo version the version field is used to determine which to return.
*
* If the higher version cannot be determined the first resource is returned.
*
* If one resource is beta and one non-beta the non-beta one is returned.
*
* @param res1 resource to compare
* @param res2 resource to compare
* @return RepositoryResource of the higher level, or res1 if the same level
* @throws RepositoryResourceValidationException
* @throws RepositoryIllegalArgumentException
*/
private RepositoryResource getNewerResource(RepositoryResource res1, RepositoryResource res2) throws RepositoryResourceValidationException {
// if one of the resources is beta and the other not, return the non-beta one
RepositoryResource singleNonBetaResource = returnNonBetaResourceOrNull(res1, res2);
if (singleNonBetaResource != null) {
return singleNonBetaResource;
}
if (res1.getType() == ResourceType.INSTALL) {
// have two String versions .. convert them into Version objects,checking that they are valid versions in the process
Version4Digit res1Version = null;
Version4Digit res2Version = null;
try {
res1Version = new Version4Digit(((ProductResourceWritable) res1).getProductVersion());
} catch (IllegalArgumentException iae) {
// the version was not a proper osgi version
throw new RepositoryResourceValidationException("The product version was invalid: " + res1Version, res1.getId(), iae);
}
try {
res2Version = new Version4Digit(((ProductResourceWritable) res2).getProductVersion());
} catch (IllegalArgumentException iae) {
// the version was not a proper osgi version
throw new RepositoryResourceValidationException("The product version was invalid: " + res2Version, res2.getId(), iae);
}
if (res1Version.compareTo(res2Version) > 0) {
return res1;
} else {
return res2;
}
} else if (res1.getType() == ResourceType.TOOL) {
// tools don't have product versions or applies to so just return res1
return res1;
} else {
return compareNonProductResourceAppliesTo(res1, res2);
}
}
/**
* Take in two resources. If one (only) is beta return the non beta one
*/
private RepositoryResource returnNonBetaResourceOrNull(RepositoryResource res1, RepositoryResource res2) {
if (isBeta(res1) && !isBeta(res2)) {
return res2;
} else if (!isBeta(res1) && isBeta(res2)) {
return res1;
} else {
return null;
}
}
private boolean isBeta(RepositoryResource res) {
String version;
String regex;
if (res.getType() == ResourceType.INSTALL) {
regex = AppliesToProcessor.BETA_REGEX;
version = ((ProductResourceWritable) res).getProductVersion();
} else if (res.getType() == ResourceType.TOOL) {
return false; // no beta tools
} else {
version = ((ApplicableToProduct) res).getAppliesTo();
regex = ".*productVersion=\"?" + AppliesToProcessor.BETA_REGEX;
}
if (version == null) {
return false;
} else {
boolean matches = version.matches(regex);
return matches;
}
}
/**
* This routine handles non product resources
*
* @param res1 - a non-product resource
* @param res2 - a non-product resource
* @return the newer resource
*/
private RepositoryResource compareNonProductResourceAppliesTo(RepositoryResource res1, RepositoryResource res2) {
// all types other than INSTALLS or TOOLS use appliesTo to determine which is the higher level
String res1AppliesTo = ((ApplicableToProduct) res1).getAppliesTo();
String res2AppliesTo = ((ApplicableToProduct) res2).getAppliesTo();
// on the basis that we will work with the appliesTo of a resource, if we find one that has an applies to
// and one that doesn't we will the one WITH the applies to is assumed to be newer (if both null we look
// at the version field)
if (res1AppliesTo == null && res2AppliesTo == null) {
// if both appliesTo are null look at the versions
return getNonProductResourceWithHigherVersion(res1, res2);
} else if (res1AppliesTo == null || res2AppliesTo == null) {
// if one of them is null we can't compare them so return res1
return res1;
}
MinAndMaxVersion res1MinMax = getMinAndMaxAppliesToVersionFromAppliesTo(res1AppliesTo);
MinAndMaxVersion res2MinMax = getMinAndMaxAppliesToVersionFromAppliesTo(res2AppliesTo);
// compare the versions and return the resource that applies to the higher minimum version
if (res1MinMax.min.compareTo(res2MinMax.min) > 0) {
return res1;
} else if (res1MinMax.min.compareTo(res2MinMax.min) == 0) {
// if they apply to the same minimum version then select the one with the highest max versions
if (res1MinMax.max.compareTo(res2MinMax.max) > 0) {
return res1;
} else if (res1MinMax.max.compareTo(res2MinMax.max) < 0) {
return res2;
} else {
// if they are still the same decide on the version
return getNonProductResourceWithHigherVersion(res1, res2);
}
} else {
return res2;
}
}
/**
* Return the resource with the highest version for when the appliesTo versions are equal
*
* @param res1 resource to compare
* @param res2 resource to compare
* @return RepositoryResource with the higher version field
*/
private RepositoryResource getNonProductResourceWithHigherVersion(RepositoryResource res1, RepositoryResource res2) {
if (res1.getVersion() == null || res2.getVersion() == null) {
return res1; // don't have two versions so can't compare
}
// have two String versions .. convert them into Version objects,checking that they are valid versions in the process
Version4Digit res1Version = null;
Version4Digit res2Version = null;
try {
res1Version = new Version4Digit(res1.getVersion());
res2Version = new Version4Digit(res2.getVersion());
} catch (IllegalArgumentException iae) {
// at least one of the one or more of Versions is not a proper osgi
// version so we cannot compare the version fields. Just return res1.
return res1;
}
if (res1Version.compareTo(res2Version) > 0) {
return res1;
} else {
return res2;
}
}
/**
* Parse an appliesTo to get the lowest and highest version that this asset applies to and
* return an object describing this.
*
* @param apliesTo the appliesTo String
* @return MinAndMaxVersion object describing the range of levels supported
*/
private MinAndMaxVersion getMinAndMaxAppliesToVersionFromAppliesTo(String appliesTo) {
List<AppliesToFilterInfo> res1Filters = AppliesToProcessor.parseAppliesToHeader(appliesTo);
Version4Digit highestVersion = null;
Version4Digit lowestVersion = null;
for (AppliesToFilterInfo f : res1Filters) {
Version4Digit vHigh = (f.getMaxVersion() == null) ? MAX_VERSION : new Version4Digit(f.getMaxVersion().getValue());
Version4Digit vLow = (f.getMinVersion() == null) ? MIN_VERSION : new Version4Digit(f.getMinVersion().getValue());
if (highestVersion == null || vHigh.compareTo(highestVersion) > 0) {
highestVersion = vHigh;
lowestVersion = vLow;
} else if (vHigh.compareTo(highestVersion) == 0) {
if (lowestVersion == null || vLow.compareTo(lowestVersion) > 0) {
highestVersion = vHigh;
lowestVersion = vLow;
}
}
}
return new MinAndMaxVersion(lowestVersion, highestVersion);
}
/**
* Returns <code>true</code> if the resource will be visible to the website.
*
* @param resource
* @return
*/
private boolean isVisibleAndWebDisplayable(RepositoryResource resource) {
if (resource instanceof WebDisplayable) {
DisplayPolicy displayPolicy = ((WebDisplayable) resource).getWebDisplayPolicy();
return displayPolicy == DisplayPolicy.VISIBLE || displayPolicy == null;
} else {
return false;
}
}
/**
* Private class to return min and max versions
*/
public static class MinAndMaxVersion {
public Version4Digit min;
public Version4Digit max;
public MinAndMaxVersion(Version4Digit minVersion, Version4Digit maxVersion) {
min = minVersion;
max = maxVersion;
}
@Override
public String toString() {
return "(min=" + min + ", max=" + max + ")";
}
}
}