/*
* 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 de.unioninvestment.eai.portal.portlet.crud.domain.model;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.unioninvestment.eai.portal.portlet.crud.config.InitializeTypeConfig;
import de.unioninvestment.eai.portal.portlet.crud.config.SelectConfig;
import de.unioninvestment.eai.portal.portlet.crud.domain.exception.TechnicalCrudPortletException;
import de.unioninvestment.eai.portal.portlet.crud.domain.support.QueryOptionListRepository;
import de.unioninvestment.eai.portal.support.vaadin.context.BackgroundThreadContextProvider;
import de.unioninvestment.eai.portal.support.vaadin.context.ContextualCallable;
import de.unioninvestment.eai.portal.support.vaadin.mvp.EventBus;
/**
*
* Modell-Klasse für Auswahl-Boxen. Sofern die Query mit
* <code>initialize="async"</code> konfiguriert wurde, wird bei der
* Initialisierung und beim Refresh die Optionsliste asynchron vorgeladen.
*
* @author max.hartmann
* @author carsten.mjartan
*
*/
public class QueryOptionList extends VolatileOptionList {
private static final long serialVersionUID = 1L;
private static final Logger LOGGER = LoggerFactory
.getLogger(QueryOptionList.class);
private volatile Future<Map<String, String>> future;
private volatile Map<String, String> options;
private String query;
private Object lock = new Object();
private String id;
private boolean lazy;
private boolean prefetched;
private boolean useCache;
private final ExecutorService prefetchExecutor;
private QueryOptionListRepository repository;
private String dataSource;
/**
* Konstruktor mit Parametern. Wird verwendet, wenn die Optionen aus der
* Daten Datenbank gelesen werden sollen.
*
* @param config
* Konfiguration der Auswahl-Box
* @param eventBus
* @param repository
* repository for query retrieval which also provides caching
* functionality
* @param asyncExecutor
* executes the prefetch operation, if configured
*/
public QueryOptionList(SelectConfig config, EventBus eventBus,
QueryOptionListRepository repository, String dataSource,
ExecutorService asyncExecutor, boolean useCacheByDefault) {
super(eventBus);
this.dataSource = dataSource;
this.prefetchExecutor = asyncExecutor;
this.repository = repository;
this.id = config.getId();
this.query = config.getQuery().getValue();
this.useCache = config.getQuery().isCached() == null ? useCacheByDefault
: config.getQuery().isCached();
InitializeTypeConfig initialize = config.getQuery().getInitialize();
this.lazy = initialize.equals(InitializeTypeConfig.LAZY)
|| initialize.equals(InitializeTypeConfig.ASYNC);
this.prefetched = initialize.equals(InitializeTypeConfig.ASYNC);
if (useCache) {
options = repository.getOptionsFromCache(dataSource, query);
}
if (options == null && prefetched) {
startPrefetch();
}
}
private void startPrefetch() {
cancelOlderPrefetch();
logIfThreadPoolIsFull(prefetchExecutor);
BackgroundThreadContextProvider provider = new BackgroundThreadContextProvider();
future = prefetchExecutor.submit(new ContextualCallable<Map<String, String>>(provider) {
@Override
public Map<String, String> callWithContext() {
LOGGER.debug("Prefetching option list {}", logId());
synchronized (lock) {
options = loadOptions();
fireChangeEvent(true);
return options;
}
}
});
}
private void logIfThreadPoolIsFull(ExecutorService prefetchExecutor) {
if (prefetchExecutor instanceof ThreadPoolExecutor) {
ThreadPoolExecutor executor = (ThreadPoolExecutor) prefetchExecutor;
int requestsInProgress = executor.getActiveCount()
+ executor.getQueue().size();
if (requestsInProgress >= executor.getMaximumPoolSize()) {
LOGGER.warn(
"Maximum OptionList prefetch threads reached (~{}/{}). Request will be queued.",
requestsInProgress, executor.getMaximumPoolSize());
}
}
}
private String logId() {
return id != null ? "'" + id + "'" : "";
}
private void cancelOlderPrefetch() {
Future<Map<String, String>> currentFuture = future;
if (currentFuture != null) {
currentFuture.cancel(true);
}
future = null;
}
/**
* {@inheritDoc}
*/
@Override
public Map<String, String> getOptions(SelectionContext context) {
synchronized (lock) {
if (options == null) {
if (future != null) {
waitForOptionsFromFuture();
future = null;
}
if (options == null) {
options = loadOptions();
}
}
return options;
}
}
private void waitForOptionsFromFuture() {
try {
future.get();
} catch (InterruptedException e) {
// Task interrupted (by refresh?) => continue reading
// synchronously
} catch (ExecutionException e) {
throw new TechnicalCrudPortletException(
"Error acquiring result of prefetch operation",
e.getCause());
}
}
/**
*
* Nur für UnitTests Liefert die Einträge. Beachten, dass die Werte nicht
* aus der DB geladen werden.
*
* @return Einträge im Objekt
*/
Map<String, String> getOptions() {
return options;
}
protected Map<String, String> loadOptions() {
LOGGER.debug("Loading option list {}", logId());
long startTime = System.currentTimeMillis();
Map<String, String> newOptions = repository.getOptions(dataSource,
query, useCache);
long duration = System.currentTimeMillis() - startTime;
LOGGER.debug("Finished loading option list {} ({}ms)", logId(),
duration);
return newOptions;
}
/**
* Entfernt die gepufferten Werte.
*/
@Override
public void refresh(RefreshPolicy policy) {
synchronized (lock) {
if (policy == RefreshPolicy.FROM_SOURCE && useCache) {
repository.remove(dataSource, query);
}
options = null;
if (prefetched) {
startPrefetch();
} else {
fireChangeEvent(false);
}
}
}
public String getId() {
return id;
}
/**
* Information für das Frontend, wann die Optionsliste aus der DB zu laden
* ist.
*/
@Override
public boolean isLazy() {
return lazy;
}
@Override
public boolean isInitialized() {
return options != null;
}
/**
* @param options
* for testing
*/
void setOptions(Map<String, String> options) {
this.options = options;
}
/**
* @return if the portlet is configured to be prefetched
*/
public boolean isPrefetched() {
return prefetched;
}
}