blob: 2d016bffc9ddf2af3fceb314957f4bcb2ce31db9 [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.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.sling.installer.api.InstallableResource;
import org.apache.sling.installer.api.event.InstallationListener;
import org.apache.sling.installer.api.tasks.RegisteredResource;
import org.apache.sling.installer.api.tasks.TransformationResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Persistent list of RegisteredResource, used by installer to
* keep track of all registered resources
*/
public class PersistentResourceList {
/** Serialization version. */
private static final int VERSION = 2;
/** Entity id for restart active bundles. */
public static final String RESTART_ACTIVE_BUNDLES_TYPE = "org.apache.sling.installer.core.restart.bundles";
public static final String RESTART_ACTIVE_BUNDLES_ID = "org.apache.sling.installer.core.restart.bundles";
public static final String RESTART_ACTIVE_BUNDLES_ENTITY_ID = RESTART_ACTIVE_BUNDLES_TYPE + ':' + RESTART_ACTIVE_BUNDLES_ID;
/** The logger */
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* Map of registered resource sets.
* The key of the map is the entity id of the registered resource.
* The value is a set containing all registered resources for the
* same entity. Usually this is just one resource per entity.
*/
private final Map<String, EntityResourceList> data;
/** The persistence file. */
private final File dataFile;
/** All untransformed resources. */
private final List<RegisteredResource> untransformedResources;
private final InstallationListener listener;
@SuppressWarnings("unchecked")
public PersistentResourceList(final File dataFile, final InstallationListener listener) {
this.dataFile = dataFile;
this.listener = listener;
Map<String, EntityResourceList> restoredData = null;
List<RegisteredResource> unknownList = null;
if ( dataFile.exists() ) {
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new BufferedInputStream(new FileInputStream(dataFile)));
final int version = ois.readInt();
if ( version > 0 && version <= VERSION ) {
restoredData = (Map<String, EntityResourceList>)ois.readObject();
if ( version == VERSION ) {
unknownList = (List<RegisteredResource>)ois.readObject();
}
} else {
logger.warn("Unknown version for persistent resource list: {}", version);
}
logger.debug("Restored resource list: {}", restoredData);
logger.debug("Restored unknown resource list: {}", unknownList);
} catch (final Exception e) {
logger.warn("Unable to restore data, starting with empty list (" + e.getMessage() + ")", e);
restoredData = null;
unknownList = null;
} finally {
if (ois != null) {
try {
ois.close();
} catch (final IOException ignore) {
// ignore
}
}
}
}
data = restoredData != null ? restoredData : new HashMap<String, EntityResourceList>();
this.untransformedResources = unknownList != null ? unknownList : new ArrayList<RegisteredResource>();
this.updateCache();
// update resource ids
for(final Map.Entry<String, EntityResourceList> entry : this.data.entrySet()) {
final EntityResourceList erl = entry.getValue();
erl.setResourceId(entry.getKey());
erl.setListener(listener);
}
// check for special resources
if ( this.getEntityResourceList(RESTART_ACTIVE_BUNDLES_ENTITY_ID) == null ) {
final RegisteredResource rr = this.addOrUpdate(new InternalResource("$sling-installer$",
RESTART_ACTIVE_BUNDLES_ID,
null,
new Hashtable<String, Object>(),
InstallableResource.TYPE_PROPERTIES, "1",
null, null, null));
final TransformationResult result = new TransformationResult();
result.setId(RESTART_ACTIVE_BUNDLES_ID);
result.setResourceType(RESTART_ACTIVE_BUNDLES_TYPE);
this.transform(rr, new TransformationResult[] {result});
}
}
/**
* Update the url to digest cache
*/
private void updateCache() {
for(final EntityResourceList group : this.data.values()) {
for(final RegisteredResource rr : group.listResources()) {
if ( ((RegisteredResourceImpl)rr).hasDataFile() ) {
FileDataStore.SHARED.updateDigestCache(rr.getURL(), ((RegisteredResourceImpl)rr).getDataFile(), rr.getDigest());
}
}
}
for(final RegisteredResource rr : this.untransformedResources ) {
if ( ((RegisteredResourceImpl)rr).hasDataFile() ) {
FileDataStore.SHARED.updateDigestCache(rr.getURL(), ((RegisteredResourceImpl)rr).getDataFile(), rr.getDigest());
}
}
}
/**
* Persist the current state
*/
public void save() {
try {
final ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(dataFile)));
try {
oos.writeInt(VERSION);
oos.writeObject(data);
oos.writeObject(untransformedResources);
logger.debug("Persisted resource list.");
} finally {
oos.close();
}
} catch (final Exception e) {
logger.warn("Unable to save persistent list: " + e.getMessage(), e);
}
}
public Collection<String> getEntityIds() {
return this.data.keySet();
}
/**
* Add or update an installable resource.
* @param input The installable resource
*/
public RegisteredResource addOrUpdate(final InternalResource input) {
// first check untransformed resource if there are resources with the same url and digest
for(final RegisteredResource rr : this.untransformedResources ) {
if ( rr.getURL().equals(input.getURL()) && ( rr.getDigest().equals(input.getDigest())) ) {
// if we found the resource we can return after updating
((RegisteredResourceImpl)rr).update(input);
return rr;
}
}
// installed resources are next
for(final EntityResourceList group : this.data.values()) {
for(final RegisteredResource rr : group.listResources()) {
if ( rr.getURL().equals(input.getURL()) && ( rr.getDigest().equals(input.getDigest()))) {
// if we found the resource we can return after updating
((RegisteredResourceImpl)rr).update(input);
return rr;
}
}
}
try {
final RegisteredResourceImpl registeredResource = RegisteredResourceImpl.create(input);
this.checkInstallable(registeredResource);
return registeredResource;
} catch (final IOException ioe) {
logger.warn("Ignoring resource. Error during processing of " + input.getURL(), ioe);
return null;
}
}
/**
* Check if the provided installable resource is already installable (has a
* known resource type)
*/
private void checkInstallable(final RegisteredResourceImpl input) {
if ( !InstallableResource.TYPE_FILE.equals(input.getType())
&& !InstallableResource.TYPE_PROPERTIES.equals(input.getType()) ) {
EntityResourceList t = this.data.get(input.getEntityId());
if (t == null) {
t = new EntityResourceList(input.getEntityId(), this.listener);
this.data.put(input.getEntityId(), t);
}
t.addOrUpdate(input);
} else {
// check if there is an old resource and remove it first
if ( this.untransformedResources.contains(input) ) {
this.untransformedResources.remove(input);
}
this.untransformedResources.add(input);
}
}
/**
* Get the list of untransformed resources = resources without resource type
*/
public List<RegisteredResource> getUntransformedResources() {
return this.untransformedResources;
}
/**
* Remove a resource by url.
* Check all resource groups and the list of untransformed resources.
* @param url The url to remove
*/
public void remove(final String url) {
// iterate over all resource groups and remove resources
// with the given url
for(final EntityResourceList group : this.data.values()) {
group.remove(url);
}
// iterate over untransformed resources and remove
// the resource with that url
final Iterator<RegisteredResource> i = this.untransformedResources.iterator();
while ( i.hasNext() ) {
final RegisteredResource rr = i.next();
if ( rr.getURL().equals(url) ) {
((RegisteredResourceImpl)rr).cleanup();
i.remove();
break;
}
}
}
/**
* Get the resource group for an entity id.
*/
public EntityResourceList getEntityResourceList(final String entityId) {
EntityResourceList erl = this.data.get(entityId);
if ( erl == null ) {
for(final EntityResourceList group : this.data.values()) {
if ( entityId.equals(group.getFullAlias()) ) {
erl = group;
break;
}
}
}
return erl;
}
/**
* Compact the internal state and remove empty groups.
* @return <code>true</code> if another cycle should be started.
*/
public boolean compact() {
boolean startNewCycle = false;
final Iterator<Map.Entry<String, EntityResourceList>> i = this.data.entrySet().iterator();
while ( i.hasNext() ) {
final Map.Entry<String, EntityResourceList> entry = i.next();
startNewCycle |= entry.getValue().compact();
if ( entry.getValue().isEmpty() ) {
i.remove();
}
}
return startNewCycle;
}
/**
* Transform an unknown resource to a registered one
*/
public void transform(final RegisteredResource resource,
final TransformationResult[] result) {
// remove resource from unknown list
this.untransformedResources.remove(resource);
try {
Set<String> entityIds = new HashSet<>();
for(int i=0; i<result.length; i++) {
// check the result
final TransformationResult tr = result[i];
if ( tr == null ) {
logger.warn("Ignoring null result for {}", resource);
continue;
}
if ( tr.getResourceType() != null && tr.getId() == null) {
logger.error("Result for {} contains new resource type {} but no unique id!",
resource, tr.getResourceType());
continue;
}
final RegisteredResourceImpl clone = (RegisteredResourceImpl)((RegisteredResourceImpl)resource).clone(result[i]);
this.checkInstallable(clone);
entityIds.add(clone.getEntityId());
}
for (EntityResourceList group : this.data.values()) {
if (!entityIds.contains(group.getResourceId())) {
if (group.removeInternal(resource.getURL())) {
logger.debug("Removed stale resources from group with entityid: {} because after transforming {} the entityids have changed.", group.getResourceId(), resource);
}
}
}
} catch (final IOException ioe) {
logger.warn("Ignoring resource. Error during processing of " + resource, ioe);
}
}
/**
* Check if the id is a special id and should not be included in the info report
*/
public boolean isSpecialEntityId(final String id) {
return RESTART_ACTIVE_BUNDLES_ENTITY_ID.equals(id);
}
public void update(final String oldId, final String newAlias, final String newId) {
final EntityResourceList list = this.data.remove(oldId);
if (list != null ) {
list.update(newAlias, newId);
this.data.put(newId, list);
}
}
}