/*
* Carrot2 project.
*
* Copyright (C) 2002-2014, Dawid Weiss, Stanisław Osiński.
* All rights reserved.
*
* Refer to the full license file "carrot2.LICENSE"
* in the root folder of the repository checkout or at:
* http://www.carrot2.org/carrot2.LICENSE
*/
package org.carrot2.webapp.model;
import java.io.InputStream;
import java.util.*;
import javax.servlet.ServletContext;
import org.carrot2.core.*;
import org.carrot2.core.attribute.AttributeNames;
import org.carrot2.core.attribute.InternalAttributePredicate;
import org.carrot2.util.CloseableUtils;
import org.carrot2.util.attribute.*;
import org.carrot2.util.resource.*;
import org.carrot2.util.resource.ResourceLookup.Location;
import org.carrot2.webapp.QueryProcessorServlet;
import org.carrot2.webapp.filter.QueryWordHighlighter;
import org.simpleframework.xml.*;
import org.simpleframework.xml.Attribute;
import org.simpleframework.xml.core.Persister;
import org.slf4j.Logger;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.*;
/**
* The application-wide configuration.
*/
public class WebappConfig
{
private final static Logger log = org.slf4j.LoggerFactory
.getLogger(WebappConfig.class);
@Element(required = false)
public ProcessingComponentSuite components;
/**
* Descriptors of attributes to display in the advanced document source options view,
* keyed by document source id.
*/
public Map<String, List<AttributeDescriptor>> sourceAttributeMetadata;
/**
* Values of document source attributes set at component initialization time, keyed by
* document source id. We need these to show proper default values in advanced options
* for those sources for which the default attribute values have been overridden in
* the component suite.
*/
public Map<String, Map<String, Object>> sourceInitializationAttributes;
/**
* A set of keys of all internal attributes of all components. We need this to prevent
* these attributes to be bound from the HTTP request parameters.
*/
public Set<String> componentInternalAttributeKeys;
@ElementList(entry = "skin")
public ArrayList<SkinModel> skins;
@ElementList(entry = "size")
public ArrayList<ResultsSizeModel> sizes;
@ElementList(entry = "view")
public ArrayList<ResultsViewModel> views;
@ElementList(entry = "cache", required = false)
public ArrayList<ResultsCacheModel> caches = Lists.newArrayList();
@Attribute(name = "skins-folder")
public String skinsFolder;
@Attribute(name = "component-suite")
public String componentSuite = "suites/suite-webapp.xml";
@Attribute(name = "search-url", required = false)
public String SEARCH_URL = "search";
@Attribute(name = "xml-url", required = false)
public String XML_URL = "xml";
@Attribute(name = "query-highlighter", required = false)
public String QUERY_HIGHLIGHTER_ID = QueryWordHighlighter.class.getName();
@Attribute(name = "max-carrot2-results", required = false)
public Integer maxCarrot2Results = null;
// These are for output serialization only, this is somewhat clumsy.
@Attribute(name = "query-param", required = false)
public final static String QUERY_PARAM = AttributeNames.QUERY;
public final static String QUERY_PARAM_ALIAS = "q";
@Attribute(name = "results-param", required = false)
public final static String RESULTS_PARAM = AttributeNames.RESULTS;
@Attribute(name = "source-param", required = false)
public final static String SOURCE_PARAM = "source";
@Attribute(name = "algorithm-param", required = false)
public final static String ALGORITHM_PARAM = "algorithm";
@Attribute(name = "type-param", required = false)
public final static String TYPE_PARAM = "type";
@Attribute(name = "view-param", required = false)
public final static String VIEW_PARAM = "view";
@Attribute(name = "skin-param", required = false)
public final static String SKIN_PARAM = "skin";
@Attribute(name = "stylesheet-param", required = false)
public final static String STYLESHEET_PARAM = "stylesheet";
/**
* @return Initialize the global configuration and return it.
*/
public final static WebappConfig getSingleton(ServletContext context)
{
try
{
// Load web application configuration.
final IResource webappConfig = new ResourceLookup(
new PrefixDecoratorLocator(new ServletContextLocator(context), "/WEB-INF/"))
.getFirst("webapp-config.xml");
if (webappConfig == null)
throw new RuntimeException("Could not find WEB-INF/webapp-config.xml.");
final WebappConfig conf = deserialize(webappConfig);
if (conf.skins.size() == 0)
{
throw new RuntimeException("Configuration must contain at least one skin");
}
if (conf.views.size() == 0)
{
throw new RuntimeException("Configuration must contain at least one view");
}
if (conf.sizes.size() == 0)
{
throw new RuntimeException(
"Configuration must contain at least one result list size");
}
// Load component suite.
List<IResourceLocator> resourceLocators = Lists.newArrayList();
resourceLocators.add(new PrefixDecoratorLocator(
new ServletContextLocator(context), "/WEB-INF/suites/"));
if (Boolean.getBoolean(QueryProcessorServlet.ENABLE_CLASSPATH_LOCATOR))
resourceLocators.add(Location.CONTEXT_CLASS_LOADER.locator);
ResourceLookup suitesLookup = new ResourceLookup(resourceLocators);
IResource suite = suitesLookup.getFirst(conf.componentSuite);
if (suite == null)
{
throw new Exception("Suite file not found in servlet context's /WEB-INF/suites: "
+ conf.componentSuite);
}
conf.components = ProcessingComponentSuite.deserialize(suite, suitesLookup);
log.info("Loaded " + conf.components.getSources().size() + " sources and "
+ conf.components.getAlgorithms().size() + " algorithms");
// Prepare attribute descriptors for document sources
conf.sourceAttributeMetadata = prepareSourceAttributeMetadata(conf.components);
conf.sourceInitializationAttributes = prepareSourceInitializationAttributes(conf.components);
conf.componentInternalAttributeKeys = prepareComponentInternalAttributeKeys(conf.components);
return conf;
}
catch (Exception e)
{
log.error("Could not load application config.", e);
throw new RuntimeException("Could not load application config.", e);
}
}
public String getContextRelativeSkinStylesheet(RequestModel requestModel)
{
return "/" + skinsFolder + "/" + requestModel.skin + "/" + requestModel.stylesheet;
}
public SkinModel getSkinById(String skinId)
{
// Short list, we can afford linear search
for (SkinModel skin : skins)
{
if (skin.id.equals(skinId))
{
return skin;
}
}
return ModelWithDefault.getDefault(skins);
}
private static WebappConfig deserialize(IResource resource) throws Exception
{
final InputStream inputStream = resource.open();
final WebappConfig loaded;
try
{
loaded = new Persister().read(WebappConfig.class, inputStream);
}
finally
{
CloseableUtils.close(inputStream);
}
return loaded;
}
@SuppressWarnings("unchecked")
private static Map<String, List<AttributeDescriptor>> prepareSourceAttributeMetadata(
ProcessingComponentSuite components) throws Exception
{
final List<DocumentSourceDescriptor> sources = components.getSources();
final Map<String, List<AttributeDescriptor>> sourceDescriptors = Maps
.newLinkedHashMap();
for (DocumentSourceDescriptor documentSourceDescriptor : sources)
{
final BindableDescriptor bindableDescriptor = documentSourceDescriptor
.getBindableDescriptor().only(Input.class).only(
new LevelsPredicate(AttributeLevel.BASIC, AttributeLevel.MEDIUM))
.only(
Predicates.<AttributeDescriptor> and(Predicates
.not(new InternalAttributePredicate()),
new Predicate<AttributeDescriptor>()
{
/** Attribute types supported in advanced source options */
final Set<Class<?>> ALLOWED_PLAIN_TYPES = ImmutableSet
.<Class<?>> of(Byte.class, Short.class, Integer.class,
Long.class, Float.class, Double.class, Boolean.class,
String.class, Character.class);
private final Set<String> IGNORED = ImmutableSet.<String> of(
AttributeNames.QUERY, AttributeNames.RESULTS);
public boolean apply(AttributeDescriptor d)
{
return (d.type.isEnum() || ALLOWED_PLAIN_TYPES
.contains(d.type))
&& !IGNORED.contains(d.key);
}
}));
final List<AttributeDescriptor> descriptors = Lists
.newArrayList(bindableDescriptor.attributeDescriptors.values());
Collections.sort(descriptors, new Comparator<AttributeDescriptor>()
{
public int compare(AttributeDescriptor d1, AttributeDescriptor d2)
{
return getOrder(d1) - getOrder(d2);
}
private int getOrder(AttributeDescriptor d)
{
if (d.type.isEnum())
{
return 0;
}
else if (d.type.equals(Boolean.class))
{
return 2;
}
else
{
return 1;
}
}
});
sourceDescriptors.put(documentSourceDescriptor.getId(), descriptors);
}
return sourceDescriptors;
}
private static Map<String, Map<String, Object>> prepareSourceInitializationAttributes(
ProcessingComponentSuite components)
{
final List<DocumentSourceDescriptor> sources = components.getSources();
final Map<String, Map<String, Object>> initAttributes = Maps.newHashMap();
for (DocumentSourceDescriptor documentSourceDescriptor : sources)
{
initAttributes.put(documentSourceDescriptor.getId(), documentSourceDescriptor
.getComponentConfiguration().attributes);
}
return initAttributes;
}
private static Set<String> prepareComponentInternalAttributeKeys(
ProcessingComponentSuite components) throws Exception
{
final List<ProcessingComponentDescriptor> descriptors = components
.getComponents();
final Set<String> internalAttributeKeys = Sets.newHashSet();
for (ProcessingComponentDescriptor descriptor : descriptors)
{
internalAttributeKeys.addAll(Lists.transform(Lists
.newArrayList(descriptor.getBindableDescriptor().only(
new InternalAttributePredicate()).attributeDescriptors.values()),
AttributeDescriptor.AttributeDescriptorToKey.INSTANCE));
}
internalAttributeKeys.remove(AttributeNames.QUERY);
return internalAttributeKeys;
}
}