blob: c6e7adfea1da3fbd99230448ea9895f288a6838d [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.solr.rest;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.core.SolrResourceLoader;
import org.apache.solr.rest.ManagedResourceStorage.StorageIO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Supports Solr components that have external data that
* needs to be managed using the REST API.
*/
public abstract class ManagedResource {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
/**
* Marker interface to indicate a ManagedResource implementation class also supports
* managing child resources at path: /<resource>/{child}
*/
public static interface ChildResourceSupport {}
public static final String INIT_ARGS_JSON_FIELD = "initArgs";
public static final String MANAGED_JSON_LIST_FIELD = "managedList";
public static final String MANAGED_JSON_MAP_FIELD = "managedMap";
public static final String INITIALIZED_ON_JSON_FIELD = "initializedOn";
public static final String UPDATED_SINCE_INIT_JSON_FIELD = "updatedSinceInit";
private final String resourceId;
protected final SolrResourceLoader solrResourceLoader;
protected final ManagedResourceStorage storage;
protected NamedList<Object> managedInitArgs;
protected Date initializedOn;
protected Date lastUpdateSinceInitialization;
/**
* Initializes this managed resource, including setting up JSON-based storage using
* the provided storageIO implementation, such as ZK.
*/
protected ManagedResource(String resourceId, SolrResourceLoader loader, StorageIO storageIO)
throws SolrException {
this.resourceId = resourceId;
this.solrResourceLoader = loader;
this.storage = createStorage(storageIO, loader);
}
/**
* Called once during core initialization to get the managed
* data loaded from storage and notify observers.
*/
public void loadManagedDataAndNotify(Collection<ManagedResourceObserver> observers)
throws SolrException {
// load managed data from storage
reloadFromStorage();
// important!!! only affect the Solr component once during core initialization
// also, as most analysis components will alter the initArgs it is processes them
// we need to clone the managed initArgs
notifyObserversDuringInit(managedInitArgs, observers);
// some basic date tracking around when the data was initialized and updated
initializedOn = new Date();
lastUpdateSinceInitialization = null;
}
/**
* Notifies all registered observers that the ManagedResource is initialized.
* This event only occurs once when the core is loaded. Thus, you need to
* reload the core to get updates applied to the analysis components that
* depend on the ManagedResource data.
*/
protected void notifyObserversDuringInit(NamedList<?> args, Collection<ManagedResourceObserver> observers)
throws SolrException {
if (observers == null || observers.isEmpty())
return;
for (ManagedResourceObserver observer : observers) {
// clone the args for each observer as some components
// remove args as they process them, e.g. AbstractAnalysisFactory
NamedList<?> clonedArgs = args.clone();
observer.onManagedResourceInitialized(clonedArgs,this);
}
if (log.isInfoEnabled()) {
log.info("Notified {} observers of {}", observers.size(), getResourceId());
}
}
/**
* Potential extension point allowing concrete implementations to supply their own storage
* implementation. The default implementation uses JSON as the storage format and delegates
* the loading and saving of JSON bytes to the supplied StorageIO class.
*/
protected ManagedResourceStorage createStorage(StorageIO storageIO, SolrResourceLoader loader)
throws SolrException {
return new ManagedResourceStorage.JsonStorage(storageIO, loader);
}
/**
* Returns the resource loader used by this resource.
*/
public SolrResourceLoader getResourceLoader() {
return solrResourceLoader;
}
/**
* Gets the resource ID for this managed resource.
*/
public String getResourceId() {
return resourceId;
}
/**
* Gets the ServerResource class to register this endpoint with the Rest API router;
* in most cases, the default RestManager.ManagedEndpoint class is sufficient but
* ManagedResource implementations can override this method if a different ServerResource
* class is needed.
*/
public Class<? extends BaseSolrResource> getServerResourceClass() {
return RestManager.ManagedEndpoint.class;
}
/**
* Called from {@link #doPut(BaseSolrResource,Object)}
* to update this resource's init args using the given updatedArgs
*/
@SuppressWarnings("unchecked")
protected boolean updateInitArgs(NamedList<?> updatedArgs) {
if (updatedArgs == null || updatedArgs.size() == 0) {
return false;
}
boolean madeChanges = false;
if (!managedInitArgs.equals(updatedArgs)) {
managedInitArgs = (NamedList<Object>)updatedArgs.clone();
madeChanges = true;
}
return madeChanges;
}
/**
* Invoked when this object determines it needs to reload the stored data.
*/
@SuppressWarnings("unchecked")
protected synchronized void reloadFromStorage() throws SolrException {
String resourceId = getResourceId();
Object data = null;
try {
data = storage.load(resourceId);
} catch (FileNotFoundException fnf) {
log.warn("No stored data found for {}", resourceId);
} catch (IOException ioExc) {
throw new SolrException(ErrorCode.SERVER_ERROR,
"Failed to load stored data for "+resourceId+" due to: "+ioExc, ioExc);
}
Object managedData = processStoredData(data);
if (managedInitArgs == null)
managedInitArgs = new NamedList<>();
onManagedDataLoadedFromStorage(managedInitArgs, managedData);
}
/**
* Processes the stored data.
*/
protected Object processStoredData(Object data) throws SolrException {
Object managedData = null;
if (data != null) {
if (!(data instanceof Map)) {
throw new SolrException(ErrorCode.SERVER_ERROR,
"Stored data for "+resourceId+" is not a valid JSON object!");
}
@SuppressWarnings({"unchecked"})
Map<String,Object> jsonMap = (Map<String,Object>)data;
@SuppressWarnings({"unchecked"})
Map<String,Object> initArgsMap = (Map<String,Object>)jsonMap.get(INIT_ARGS_JSON_FIELD);
managedInitArgs = new NamedList<>(initArgsMap);
log.info("Loaded initArgs {} for {}", managedInitArgs, resourceId);
if (jsonMap.containsKey(MANAGED_JSON_LIST_FIELD)) {
Object jsonList = jsonMap.get(MANAGED_JSON_LIST_FIELD);
if (!(jsonList instanceof List)) {
String errMsg =
String.format(Locale.ROOT,
"Expected JSON array as value for %s but client sent a %s instead!",
MANAGED_JSON_LIST_FIELD, jsonList.getClass().getName());
throw new SolrException(ErrorCode.SERVER_ERROR, errMsg);
}
managedData = jsonList;
} else if (jsonMap.containsKey(MANAGED_JSON_MAP_FIELD)) {
Object jsonObj = jsonMap.get(MANAGED_JSON_MAP_FIELD);
if (!(jsonObj instanceof Map)) {
String errMsg =
String.format(Locale.ROOT,
"Expected JSON map as value for %s but client sent a %s instead!",
MANAGED_JSON_MAP_FIELD, jsonObj.getClass().getName());
throw new SolrException(ErrorCode.SERVER_ERROR, errMsg);
}
managedData = jsonObj;
}
}
return managedData;
}
/**
* Method called after data has been loaded from storage to give the concrete
* implementation a chance to post-process the data.
*/
protected abstract void onManagedDataLoadedFromStorage(NamedList<?> managedInitArgs, Object managedData)
throws SolrException;
/**
* Persists managed data to the configured storage IO as a JSON object.
*/
public synchronized void storeManagedData(Object managedData) {
Map<String,Object> toStore = buildMapToStore(managedData);
String resourceId = getResourceId();
try {
storage.store(resourceId, toStore);
// keep track that the managed data has been updated
lastUpdateSinceInitialization = new Date();
} catch (Throwable storeErr) {
// store failed, so try to reset the state of this object by reloading
// from storage and then failing the store request, but only do that
// if we've successfully initialized before
if (initializedOn != null) {
try {
reloadFromStorage();
} catch (Exception reloadExc) {
// note: the data we're managing now remains in a dubious state
// however the text analysis component remains unaffected
// (at least until core reload)
log.error("Failed to load data from storage due to: {}", reloadExc);
}
}
String errMsg = String.format(Locale.ROOT,
"Failed to store data for %s due to: %s",
resourceId, storeErr.toString());
log.error(errMsg, storeErr);
throw new SolrException(ErrorCode.SERVER_ERROR, errMsg, storeErr);
}
}
/**
* Returns this resource's initialization timestamp.
*/
public String getInitializedOn() {
return initializedOn == null ? null : initializedOn.toInstant().toString();
}
/**
* Returns the timestamp of the most recent update,
* or null if this resource has not been updated since initialization.
*/
public String getUpdatedSinceInitialization() {
return lastUpdateSinceInitialization == null ? null : lastUpdateSinceInitialization.toInstant().toString();
}
/**
* Returns true if this resource has been changed since initialization.
*/
public boolean hasChangesSinceInitialization() {
return (lastUpdateSinceInitialization != null);
}
/**
* Builds the JSON object to be stored, containing initArgs and managed data fields.
*/
protected Map<String,Object> buildMapToStore(Object managedData) {
Map<String,Object> toStore = new LinkedHashMap<>(4, 1.0f);
toStore.put(INIT_ARGS_JSON_FIELD, convertNamedListToMap(managedInitArgs));
// report important dates when data was init'd / updated
String initializedOnStr = getInitializedOn();
if (initializedOnStr != null) {
toStore.put(INITIALIZED_ON_JSON_FIELD, initializedOnStr);
}
// if the managed data has been updated since initialization (ie. it's dirty)
// return that in the response as well ... which gives a good hint that the
// client needs to re-load the collection / core to apply the updates
if (hasChangesSinceInitialization()) {
toStore.put(UPDATED_SINCE_INIT_JSON_FIELD, getUpdatedSinceInitialization());
}
if (managedData != null) {
if (managedData instanceof List || managedData instanceof Set) {
toStore.put(MANAGED_JSON_LIST_FIELD, managedData);
} else if (managedData instanceof Map) {
toStore.put(MANAGED_JSON_MAP_FIELD, managedData);
} else {
throw new IllegalArgumentException(
"Invalid managed data type "+managedData.getClass().getName()+
"! Only List, Set, or Map objects are supported by this ManagedResource!");
}
}
return toStore;
}
/**
* Converts a NamedList&lt;?&gt; into an ordered Map for returning as JSON.
*/
protected Map<String,Object> convertNamedListToMap(NamedList<?> args) {
Map<String,Object> argsMap = new LinkedHashMap<>();
if (args != null) {
for (Map.Entry<String,?> entry : args) {
argsMap.put(entry.getKey(), entry.getValue());
}
}
return argsMap;
}
/**
* Just calls {@link #doPut(BaseSolrResource,Object)};
* override to change the behavior of POST handling.
*/
public void doPost(BaseSolrResource endpoint, Object json) {
doPut(endpoint, json);
}
/**
* Applies changes to initArgs or managed data.
*/
@SuppressWarnings("unchecked")
public synchronized void doPut(BaseSolrResource endpoint, Object json) {
if (log.isInfoEnabled()) {
log.info("Processing update to {}: {} is a {}", getResourceId(), json, json.getClass().getName());
}
boolean updatedInitArgs = false;
Object managedData = null;
if (json instanceof Map) {
// hmmmm ... not sure how flexible we want to be here?
Map<String,Object> jsonMap = (Map<String,Object>)json;
if (jsonMap.containsKey(INIT_ARGS_JSON_FIELD) ||
jsonMap.containsKey(MANAGED_JSON_LIST_FIELD) ||
jsonMap.containsKey(MANAGED_JSON_MAP_FIELD))
{
Map<String,Object> initArgsMap = (Map<String,Object>)jsonMap.get(INIT_ARGS_JSON_FIELD);
updatedInitArgs = updateInitArgs(new NamedList<>(initArgsMap));
if (jsonMap.containsKey(MANAGED_JSON_LIST_FIELD)) {
managedData = jsonMap.get(MANAGED_JSON_LIST_FIELD);
} else if (jsonMap.containsKey(MANAGED_JSON_MAP_FIELD)) {
managedData = jsonMap.get(MANAGED_JSON_MAP_FIELD);
}
} else {
managedData = jsonMap;
}
} else if (json instanceof List) {
managedData = json;
} else {
throw new SolrException(ErrorCode.BAD_REQUEST,
"Unsupported update format "+json.getClass().getName());
}
Object updated = null;
if (managedData != null) {
updated = applyUpdatesToManagedData(managedData);
}
if (updatedInitArgs || updated != null) {
storeManagedData(updated);
}
// PUT just returns success status code with an empty body
}
/**
* Called by the RestManager framework after this resource has been deleted
* to allow this resource to close and clean-up any resources used by this.
*
* @throws IOException if an error occurs in the underlying storage when
* trying to delete
*/
public void onResourceDeleted() throws IOException {
storage.delete(resourceId);
}
/**
* Called during PUT/POST processing to apply updates to the managed data passed from the client.
*/
protected abstract Object applyUpdatesToManagedData(Object updates);
/**
* Called to delete a named part (the given childId) of the
* resource at the given endpoint
*/
public abstract void doDeleteChild(BaseSolrResource endpoint, String childId);
/**
* Called to retrieve a named part (the given childId) of the
* resource at the given endpoint
*/
public abstract void doGet(BaseSolrResource endpoint, String childId);
}