blob: e0be1bb47e649b53dcb7a79e0cf3d92722b906da [file] [log] [blame]
/*
* 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.apache.sling.installer.core.impl;
import java.io.IOException;
import java.io.Serializable;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.apache.sling.installer.api.InstallableResource;
import org.apache.sling.installer.api.event.InstallationEvent;
import org.apache.sling.installer.api.event.InstallationListener;
import org.apache.sling.installer.api.tasks.RegisteredResource;
import org.apache.sling.installer.api.tasks.ResourceState;
import org.apache.sling.installer.api.tasks.TaskResource;
import org.apache.sling.installer.api.tasks.TaskResourceGroup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Persistent list of RegisteredResource, used by installer to
* keep track of all registered resources
*/
public class EntityResourceList implements Serializable, TaskResourceGroup {
/** Use own serial version ID as we control serialization. */
private static final long serialVersionUID = 6L;
/** Serialization version. */
private static final int VERSION = 2;
/** Logger. */
private static final Logger LOGGER = LoggerFactory.getLogger(EntityResourceList.class);
/** The list of registered resources for this entity. */
private final List<RegisteredResourceImpl> resources = new ArrayList<>();
/** Alias for this id. */
private String alias;
/** The resource id of this group. */
private String resourceId;
/** Lock */
private final Object lock = new Object();
/** The listener. */
private transient InstallationListener listener;
public EntityResourceList(final String resourceId, final InstallationListener listener) {
this.resourceId = resourceId;
this.listener = listener;
}
/**
* Serialize the object
* - write version id
* - serialize each entry in the resources list
* @param out Object output stream
* @throws IOException
*/
private void writeObject(final java.io.ObjectOutputStream out)
throws IOException {
out.writeInt(VERSION);
out.writeInt(resources.size());
for(final RegisteredResource rr : this.resources) {
out.writeObject(rr);
}
out.writeObject(this.alias);
out.writeObject(this.resourceId);
}
/**
* Deserialize the object
* - read version id
* - deserialize each entry in the resources list
*/
private void readObject(final java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException {
final int version = in.readInt();
if ( version < 1 || version > VERSION ) {
throw new ClassNotFoundException(this.getClass().getName());
}
Util.setField(this, "resources", new ArrayList<RegisteredResourceImpl>());
final int size = in.readInt();
for(int i=0; i < size; i++) {
final RegisteredResourceImpl rr = (RegisteredResourceImpl)in.readObject();
this.resources.add(rr);
}
if ( version > 1 ) {
this.alias = (String)in.readObject();
this.resourceId = (String)in.readObject();
}
Util.setField(this, "lock", new Object());
}
/**
* The resource list is empty if it contains no resources.
*/
public boolean isEmpty() {
synchronized ( lock ) {
return resources.isEmpty();
}
}
/**
* @see org.apache.sling.installer.api.tasks.TaskResourceGroup#getActiveResource()
*/
@Override
public TaskResource getActiveResource() {
synchronized ( lock ) {
if ( !resources.isEmpty() ) {
Collections.sort(this.resources);
final TaskResource r = resources.get(0);
if ( r.getState() == ResourceState.INSTALL
|| r.getState() == ResourceState.UNINSTALL ) {
return r;
}
}
}
return null;
}
/**
* @see org.apache.sling.installer.api.tasks.TaskResourceGroup#getNextActiveResource()
*/
@Override
public TaskResource getNextActiveResource() {
synchronized ( lock ) {
if ( this.getActiveResource() != null ) {
if ( this.resources.size() > 1 ) {
// to get the second item in the set we have to use an iterator!
final Iterator<RegisteredResourceImpl> i = this.resources.iterator();
i.next(); // skip first
return i.next();
}
}
}
return null;
}
/**
* Return an iterator containing all active resources in the group
*/
public Iterator<TaskResource> getActiveResourceIterator(){
synchronized ( lock ) {
if ( this.getActiveResource() != null && this.resources.size() > 1 ) {
final List<TaskResource> taskResourceList = new LinkedList<TaskResource>(this.resources);
return taskResourceList.iterator();
}
}
return null;
}
/**
* Return the first resource or null
*/
public TaskResource getFirstResource() {
synchronized ( lock ) {
if ( !resources.isEmpty() ) {
Collections.sort(this.resources);
return resources.get(0);
}
}
return null;
}
/**
* Get the alias for this group or null
*/
@Override
public String getAlias() {
return this.alias;
}
/**
* Get the alias for this group or null
*/
public String getFullAlias() {
if ( this.alias != null ) {
final int pos = this.resourceId.indexOf(':');
return this.resourceId.substring(0, pos + 1) + this.alias;
}
return null;
}
/**
* Get the resource id.
*/
public String getResourceId() {
return this.resourceId;
}
/**
* Set the resource id
*/
public void setResourceId(final String id) {
this.resourceId = id;
}
/**
* Set the listener
*/
public void setListener(final InstallationListener listener) {
this.listener = listener;
}
/**
* Force the state to be set
*/
public void setForceFinishState(final ResourceState state, String error) {
// We first set the state of the resource to install to make setFinishState work in all cases
((RegisteredResourceImpl)getFirstResource()).setState(ResourceState.INSTALL, null);
this.setFinishState(state, error);
}
/**
* @see org.apache.sling.installer.api.tasks.TaskResourceGroup#setFinishState(org.apache.sling.installer.api.tasks.ResourceState)
*/
@Override
public void setFinishState(ResourceState state) {
setFinishState(state, null);
}
@Override
public void setFinishState(ResourceState state, String error) {
final TaskResource toActivate = getActiveResource();
if ( toActivate != null ) {
synchronized ( lock ) {
if ( toActivate.getState() == ResourceState.UNINSTALL
&& this.resources.size() > 1 ) {
final TaskResource second = this.getNextActiveResource();
// check for template
if ( second.getDictionary() != null
&& second.getDictionary().get(InstallableResource.RESOURCE_IS_TEMPLATE) != null ) {
// second resource is a template! Do not install
((RegisteredResourceImpl)second).setState(ResourceState.IGNORED, null);
} else if ( state == ResourceState.UNINSTALLED ) {
// first resource got uninstalled, go back to second
if (second.getState() == ResourceState.IGNORED || second.getState() == ResourceState.INSTALLED) {
LOGGER.debug("Reactivating for next cycle: {}", second);
((RegisteredResourceImpl)second).setState(ResourceState.INSTALL, null);
}
} else {
// don't install as the first did not get uninstalled
if ( second.getState() == ResourceState.INSTALL ) {
String message = MessageFormat.format(
"The first resource '{0}' did not get uninstalled, therefore ignore this secondary resource in the uninstall group",
toActivate.getEntityId());
LOGGER.debug(message);
((RegisteredResourceImpl) second).setState(ResourceState.IGNORED, message);
}
// and now set resource to uninstalled
state = ResourceState.UNINSTALLED;
}
} else if ( state == ResourceState.INSTALLED ) {
// make sure that no other resource has state INSTALLED
if ( this.resources.size() > 1 ) {
// to get the second item in the set we have to use an iterator!
final Iterator<RegisteredResourceImpl> i = this.resources.iterator();
i.next(); // skip first
while ( i.hasNext() ) {
final TaskResource rsrc = i.next();
if ( rsrc.getState() == ResourceState.INSTALLED ) {
((RegisteredResourceImpl)rsrc).setState(ResourceState.INSTALL, "Another resource with the same entity id but a higher version or priority or digest found (in that order, the latter only in case the version is a SNAPSHOT)!");
}
}
}
}
((RegisteredResourceImpl)toActivate).setState(state, error);
if ( state != ResourceState.INSTALLED ) {
// make sure to remove all install info attributes if the resource is not
// installed anymore
toActivate.setAttribute(TaskResource.ATTR_INSTALL_EXCLUDED, null);
toActivate.setAttribute(TaskResource.ATTR_INSTALL_INFO, null);
}
// remove install info attributes on all other resources in the group
final Iterator<RegisteredResourceImpl> tri = this.resources.iterator();
tri.next(); // skip first
while ( tri.hasNext() ) {
final TaskResource rsrc = tri.next();
rsrc.setAttribute(TaskResource.ATTR_INSTALL_EXCLUDED, null);
rsrc.setAttribute(TaskResource.ATTR_INSTALL_INFO, null);
}
}
this.listener.onEvent(new InstallationEvent() {
@Override
public TYPE getType() {
return TYPE.PROCESSED;
}
@Override
public Object getSource() {
return toActivate;
}
});
if ( state == ResourceState.UNINSTALLED ) {
this.cleanup(toActivate);
}
}
}
/**
* @see org.apache.sling.installer.api.tasks.TaskResourceGroup#setFinishState(org.apache.sling.installer.api.tasks.ResourceState, java.lang.String)
*/
@Override
public void setFinishState(final ResourceState state, final String alias, String error) {
if ( this.alias == null || alias != null ) {
this.alias = alias;
}
this.setFinishState(state, error);
}
private void cleanup(final RegisteredResource rr) {
if ( rr instanceof RegisteredResourceImpl ) {
((RegisteredResourceImpl)rr).cleanup();
}
}
public Collection<RegisteredResourceImpl> listResources() {
synchronized ( lock ) {
Collections.sort(this.resources);
return resources;
}
}
public Collection<RegisteredResourceImpl> getResources() {
final List<RegisteredResourceImpl> list;
synchronized ( lock ) {
list = new ArrayList<>(this.resources);
}
Collections.sort(list);
return list;
}
public void addOrUpdate(final RegisteredResourceImpl r) {
synchronized ( lock ) {
LOGGER.debug("Adding new resource: {}", r);
Collections.sort(this.resources);
// If an object with same url is already present, replace with the
// new one which might have different attributes
boolean first = true;
boolean add = true;
final Iterator<RegisteredResourceImpl> taskIter = this.resources.iterator();
while ( taskIter.hasNext() ) {
final TaskResource rr = taskIter.next();
if ( rr.getURL().equals(r.getURL()) ) {
if ( RegisteredResourceImpl.isSameResource((RegisteredResourceImpl)rr, r) ) {
// this check is questionable. The digest could be the same and
// the resource URI might have changed nevertheless
if ( !rr.getDigest().equals(r.getDigest()) ) {
// same resource but different digest
LOGGER.debug("Updating resource due to different digests: {} vs {}", r, rr);
// we need to check the resource URI if provided (aka getDataURI())
// if different that one needs to be updated
// this update only handles the following cases:
// 1. existing resource has a resource URI and new resource has a resource URI
// 2. existing resource has a file and new resource has a resource URI
// It doesn't necessarily handle:
// 1. existing resource has a resource URI and new resource has a file
// 2. existing resource has a file and new resource has a file
((RegisteredResourceImpl)rr).updateResourceUri(r.getDataURI());
LOGGER.debug("Cleanup duplicate resource: {}", r);
this.cleanup(r);
}
// same resource, just ignore the new one
add = false;
} else {
if ( first && rr.getState() == ResourceState.INSTALLED) {
// it's not the same, but the first one is installed, so uninstall
String message = MessageFormat.format(
"The first resource '{0}' got installed, therefore uninstall this secondary resource in this group",
rr.getEntityId());
LOGGER.debug(message);
((RegisteredResourceImpl) rr).setState(ResourceState.UNINSTALL, message);
} else {
LOGGER.debug("Cleanup obsolete resource: {}", rr);
taskIter.remove();
this.cleanup(rr);
}
}
break;
}
first = false;
}
if ( add ) {
resources.add(r);
// make sure that in case the newly added resource is not the first one the state is saying why it is in mode install
Collections.sort(this.resources);
if (r != resources.get(0)) {
// don't change actual state but indicate why resource is not considered for subsequent runs (not on first position)
r.setState(ResourceState.INSTALL, "Another resource with the same entity id but a higher version or priority or digest found (in that order, the latter only in case the version is a SNAPSHOT)!");
}
}
}
}
public void remove(final String url) {
removeInternal(url);
}
boolean removeInternal(final String url) {
boolean removed = false;
synchronized ( lock ) {
Collections.sort(this.resources);
final Iterator<RegisteredResourceImpl> i = resources.iterator();
boolean first = true;
while ( i.hasNext() ) {
final TaskResource r = i.next();
if ( r.getURL().equals(url) ) {
removed = true;
if ( first && (r.getState() == ResourceState.INSTALLED
|| r.getState() == ResourceState.INSTALL)) {
LOGGER.debug("Marking for uninstalling: {}", r);
((RegisteredResourceImpl)r).setState(ResourceState.UNINSTALL, null);
} else {
LOGGER.debug("Removing unused: {}", r);
i.remove();
this.cleanup(r);
}
}
first = false;
}
}
return removed;
}
/**
* Compact the resource group by removing uninstalled entries
* @return <code>true</code> if another cycle should be started.
*/
public boolean compact() {
synchronized ( lock ) {
Collections.sort(this.resources);
boolean startNewCycle = false;
final List<TaskResource> toDelete = new ArrayList<>();
boolean first = true;
for(final TaskResource r : resources) {
if ( r.getState() == ResourceState.UNINSTALLED || (!first && r.getState() == ResourceState.UNINSTALL) ) {
toDelete.add(r);
}
first = false;
}
if (!toDelete.isEmpty()) {
// Avoid resources.remove(r) as the resource might have
// changed since it was added, which causes it to compare()
// differently and trip the TreeSet.remove() search.
final Set<RegisteredResourceImpl> copy = new HashSet<>(resources);
for(final RegisteredResource r : toDelete) {
copy.remove(r);
this.cleanup(r);
LOGGER.debug("Removing uninstalled from list: {}", r);
}
resources.clear();
resources.addAll(copy);
if ( !this.isEmpty() ) {
startNewCycle = true;
}
}
return startNewCycle;
}
}
public void update(final String newAlias, final String newResourceId) {
this.alias = newAlias;
this.resourceId = newResourceId;
for(final RegisteredResourceImpl rsrc : this.getResources()) {
rsrc.updateEntityId(newResourceId);
}
}
}