/*
* #%L
* server
* %%
* Copyright (C) 2012 - 2015 valdasraps
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Lesser Public License for more details.
*
* You should have received a copy of the GNU General Lesser Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/lgpl-3.0.html>.
* #L%
*/
package lt.emasina.resthub.server.factory;
import com.google.inject.persist.Transactional;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.inject.Inject;
import javax.inject.Singleton;
import lombok.Getter;
import lombok.extern.log4j.Log4j;
import lt.emasina.resthub.TableFactory;
import lt.emasina.resthub.factory.TableBuilder;
import org.apache.commons.beanutils.BeanUtils;
import lt.emasina.resthub.server.table.TableId;
import lt.emasina.resthub.server.table.ServerTable;
import lt.emasina.resthub.model.MdTable;
import org.json.JSONObject;
@Log4j
@Singleton
public class MetadataFactory implements MetadataFactoryIf {
private final static String ERROR_METADATA_KEY = "Error";
private final TableMaps tables = new TableMaps();
@Inject
private ResourceFactory rf;
@Inject
private QueryFactory qf;
@Inject
private TableFactory tfhead;
@Inject
private TableBuilder tb;
@Getter
private boolean forceRefresh = true;
@Transactional
@Override
public synchronized void refresh() throws Exception {
TableFactory tf = tfhead;
while (tf != null) {
boolean doRefresh = forceRefresh || tf.isRefresh();
if (log.isDebugEnabled()) {
log.debug(String.format("tf = %s, forceRefresh = %s, doRefresh = %s", tf, forceRefresh, doRefresh));
}
// Update is needed!
if (doRefresh) {
// Tables to be included into map
TableMaps toload = new TableMaps();
// Update or add tables
for (MdTable t : tf.getTables()) {
TableId id = new TableId(t);
ServerTable st = rf.create(t, tf);
st.getTable().getMetadata().remove(ERROR_METADATA_KEY);
try {
// Check the table
tb.collectColumns(t.getConnectionName(), t.getSql(), t.getColumns());
tb.collectParameters(t.getSql(), t.getParameters());
if (!toload.addTable(tf, id, st)) {
log.warn(String.format("Duplicate table definition, skipping: %s", t));
}
} catch (Exception ex) {
if (log.isDebugEnabled()) {
log.debug(String.format("Error while adding table, skipping: %s", id), ex);
} else {
log.warn(String.format("Error while adding table, skipping: %s: %s", id, ex.getMessage()));
}
toload.addBlacklist(tf, id, st);
st.getTable().getMetadata().put(ERROR_METADATA_KEY, ex.getMessage());
}
}
tables.load(tf, toload);
}
tf = tf.getNext();
}
forceRefresh = false;
}
@Override
public Collection<ServerTable> getTables() {
return Collections.unmodifiableCollection(tables.whitelist.values());
}
public Collection<ServerTable> getBlacklist() {
return Collections.unmodifiableCollection(tables.blacklist.values());
}
public ServerTable getBlacklistTable(TableId id) {
return tables.blacklist.get(id);
}
public void removeBlacklistTable(TableId id) {
tables.remove(id);
forceRefresh = true;
}
public void clearBlacklist() {
tables.clearBlacklist();
forceRefresh = true;
}
public void clearBlacklist(String namespace) {
for (TableId id : tables.blacklist.keySet()) {
if (id.getNamespace().equals(namespace)) {
tables.remove(id);
}
}
forceRefresh = true;
}
@Override
public ServerTable getTable(TableId id) {
return tables.whitelist.get(id);
}
@Override
public boolean hasTable(TableId id) {
return tables.whitelist.containsKey(id);
}
public static JSONObject mapToJSONObject(Map<?, ?> map) {
if (map == null || map.isEmpty()) {
return null;
}
return new JSONObject(map);
}
public static void injectPrivateField(Object o, Class<?> fieldHolderClass, String fieldName, Object value) throws Exception {
Field fResourceMd = fieldHolderClass.getDeclaredField(fieldName);
fResourceMd.setAccessible(true);
fResourceMd.set(o, value);
}
public static JSONObject beanToJSONObject(Object bean) throws Exception {
if (bean == null) {
return null;
}
JSONObject o = new JSONObject();
Map<?, ?> describe = BeanUtils.describe(bean);
for (Object k : describe.keySet()) {
if (k instanceof String) {
String ks = (String) k;
if (!ks.equals("class")) {
o.putOpt(ks, describe.get(k));
}
}
}
return o;
}
private class TableMaps {
private final Map<TableFactory, Set<TableId>> tfs = new ConcurrentHashMap<>();
private final Map<TableId, ServerTable> whitelist = new ConcurrentHashMap<>();
private final Map<TableId, ServerTable> blacklist = new ConcurrentHashMap<>();
/**
* Add table to good table list
* @param tf TableFactory
* @param id Table identifier
* @param st Server table
* @return true if added, false if it already exists
*/
public boolean addTable(TableFactory tf, TableId id, ServerTable st) {
// This table already exists in this batch - skip it
if (this.whitelist.containsKey(id)) {
return false;
}
if (!this.tfs.containsKey(tf)) {
this.tfs.put(tf, new HashSet<TableId>());
}
this.tfs.get(tf).add(id);
this.whitelist.put(id, st);
this.blacklist.remove(id);
return true;
}
/**
* Add table to bad table list
* @param tf TableFactory
* @param id Table identifier
* @param st Server table
* @return true if added, false if it already exists
*/
public boolean addBlacklist(TableFactory tf, TableId id, ServerTable st) {
if (this.blacklist.containsKey(id)) {
return false;
}
if (!this.tfs.containsKey(tf)) {
this.tfs.put(tf, new HashSet<TableId>());
}
this.tfs.get(tf).add(id);
this.blacklist.put(id, st);
this.whitelist.remove(id);
return true;
}
public void remove(TableId id) {
ServerTable st = blacklist.remove(id);
if (st == null) st = whitelist.remove(id);
if (st != null) {
TableFactory tf = st.getTf();
tfs.get(tf).remove(id);
if (tfs.get(tf).isEmpty()) {
tfs.remove(tf);
}
}
}
public void clearBlacklist() {
Set<TableId> toremove = new HashSet<>(blacklist.keySet());
for (TableId id: toremove) {
remove(id);
}
}
public void load(TableFactory tf, TableMaps toload) {
Set<TableId> old = tfs.get(tf);
if (toload.tfs.get(tf) != null) {
tfs.put(tf, toload.tfs.get(tf));
} else {
// TF did not return any tables while refreshing
// so remove from current table list
tfs.remove(tf);
}
if (tfs.containsKey(tf)) {
for (TableId id: tfs. get(tf)) {
if (old != null) {
for (TableId oldid: old) {
if (oldid.equals(id) && tables.whitelist.containsKey(oldid)) {
ServerTable oldTable = tables.whitelist.get(oldid);
ServerTable st = toload.whitelist.get(oldid);
if (st.isSame(oldTable)) {
break;
} else {
qf.removeQueries(id);
}
}
}
} else {
qf.removeQueries(id);
}
if (old != null && old.contains(id)) {
if (toload.whitelist.containsKey(id)) {
if (this.whitelist.containsKey(id)) {
log.info(String.format("Updating table in white list: %s", id));
} else {
log.info(String.format("Moving table to white list: %s", id));
this.blacklist.remove(id);
}
this.whitelist.put(id, toload.whitelist.get(id));
} else {
if (this.blacklist.containsKey(id)) {
log.info(String.format("Updating table in black list: %s", id));
} else {
log.warn(String.format("Moving table to black list: %s", id));
this.whitelist.remove(id);
}
this.blacklist.put(id, toload.blacklist.get(id));
}
} else {
if (toload.whitelist.containsKey(id)) {
this.whitelist.put(id, toload.whitelist.get(id));
log.info(String.format("Adding table to white list: %s", id));
} else {
this.blacklist.put(id, toload.blacklist.get(id));
log.warn(String.format("Adding table to black list: %s", id));
}
}
}
}
if (old != null) {
for (TableId id: old) {
if (!tfs.containsKey(tf) || !tfs.get(tf).contains(id)) {
qf.removeQueries(id);
if (this.whitelist.remove(id) != null) {
log.warn(String.format("Table removed from white list: %s", id));
} else {
this.blacklist.remove(id);
log.warn(String.format("Table removed from black table list: %s", id));
}
}
}
}
}
}
}