/* * Copyright 2013 Gordon Burgett and individual contributors * * Licensed 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.xflatdb.xflat.db; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.WeakHashMap; import org.apache.commons.logging.LogFactory; import org.xflatdb.xflat.Cursor; import org.xflatdb.xflat.DuplicateKeyException; import org.xflatdb.xflat.KeyNotFoundException; import org.xflatdb.xflat.Table; import org.xflatdb.xflat.XFlatException; import org.xflatdb.xflat.convert.ConversionException; import org.xflatdb.xflat.convert.ConversionService; import org.xflatdb.xflat.query.XPathQuery; import org.xflatdb.xflat.query.XPathUpdate; import org.jdom2.Element; import org.jdom2.xpath.XPathExpression; import org.xflatdb.xflat.XFlatConstants; /** * A table implementation that converts objects to elements * using the database's conversion service. * @author gordon */ public class ConvertingTable<T> extends TableBase implements Table<T> { private Class<T> tableType; /** * Gets the class of the items in the table. * @return The class object */ protected Class<T> getTableType(){ return this.tableType; } private ConversionService conversionService; public void setConversionService(ConversionService conversionService) { this.conversionService = conversionService; } private final IdAccessor accessor; private final Map<T, String> idMap; private XPathExpression<Object> alternateIdExpression; void setAlternateIdExpression(XPathExpression<Object> expression){ this.alternateIdExpression = expression; } ConvertingTable(Class<T> type, String name){ super(name); this.tableType = type; this.accessor = IdAccessor.forClass(type); if(!this.accessor.hasId()){ //we need to keep a reference to the ID in a weak cache idMap = Collections.synchronizedMap(new WeakHashMap<T, String>()); } else{ idMap = null; } } //<editor-fold desc="helpers"> private String getId(Element rowData){ return rowData.getAttributeValue("id", XFlatConstants.xFlatNs); } private String getId(T data){ if(this.accessor.hasId()){ Object id; try { id = this.accessor.getIdValue(data); } catch (IllegalAccessException | InvocationTargetException ex) { throw new XFlatException("Cannot access ID on data", ex); } if(id == null) return null; return this.getIdGenerator().idToString(id); } else if(this.idMap != null){ //hopefully we cached it return this.idMap.get(data); } return null; } private void setId(Element rowData, String sId){ rowData.setAttribute("id", sId, XFlatConstants.xFlatNs); } private Element convert(T data, String id){ Element ret; try { ret = this.conversionService.convert(data, Element.class); } catch (ConversionException ex) { throw new XFlatException("Cannot convert data with ID " + id, ex); } if (id != null){ setId(ret, id); } return ret; } private T convert(Element rowData){ String sId = getId(rowData); T ret; try { ret = this.conversionService.convert(rowData, this.getTableType()); } catch (ConversionException ex) { throw new XFlatException("Cannot convert data with ID " + sId, ex); } if(sId == null){ //can't do any ID stuff, return ret. return ret; } if(!this.accessor.hasId() && this.idMap != null){ //cache the ID this.idMap.put(ret, sId); } else{ try { this.accessor.setIdValue(ret, this.getIdGenerator().stringToId(sId, this.accessor.getIdType())); } catch (IllegalAccessException | InvocationTargetException ex) { LogFactory.getLog(getClass()).warn("Exception setting ID value " + sId + " on type " + this.accessor.getIdType(), ex); } } return ret; } private String getOrGenerateId(T rowData){ if(this.accessor.hasId()){ try{ Object id = this.accessor.getIdValue(rowData); if(id != null){ //already have an ID return this.getIdGenerator().idToString(id); } id = this.getIdGenerator().generateNewId(this.accessor.getIdType()); this.accessor.setIdValue(rowData, id); return this.getIdGenerator().idToString(id); } catch (IllegalAccessException | InvocationTargetException ex) { throw new XFlatException("Cannot generate ID", ex); } } else{ //if no ID property, always use string if(this.idMap != null){ //doublecheck in the ID map String id; synchronized(this.idMap){ id = this.idMap.get(rowData); if(id == null){ id = (String)this.getIdGenerator().generateNewId(String.class); this.idMap.put(rowData, id); } } return id; } return (String)this.getIdGenerator().generateNewId(String.class); } } //</editor-fold> @Override public void insert(T row) throws DuplicateKeyException { //generate new ID final String id = getOrGenerateId(row); final Element e = convert(row, id); this.doWithEngine(new EngineActionEx<Object, DuplicateKeyException>(){ @Override public Object act(Engine engine) throws DuplicateKeyException { engine.insertRow(id, e); return null; } }); } @Override public T find(Object id) { final String sId = this.getIdGenerator().idToString(id); Element data = this.doWithEngine(new EngineAction<Element>(){ @Override public Element act(Engine engine) { return engine.readRow(sId); } }); if(data == null){ return null; } T ret = convert(data); return ret; } @Override public T findOne(final XPathQuery query) { Element e = findOneElement(query); if(e == null){ return null; } return convert(e); } private Element findOneElement(XPathQuery query){ try(Cursor<Element> elements = this.queryTable(query)){ Iterator<Element> it = elements.iterator(); if(!it.hasNext()){ return null; } return it.next(); } catch(Exception ex){ throw new XFlatException("Unable to close cursor", ex); } } private Cursor<Element> queryTable(final XPathQuery query){ query.setAlternateIdExpression(alternateIdExpression); return this.doWithEngine(new EngineAction<Cursor<Element>>(){ @Override public Cursor<Element> act(Engine engine) { return engine.queryTable(query); } }); } @Override public Cursor<T> find(final XPathQuery query) { query.setAlternateIdExpression(alternateIdExpression); return this.doWithEngine(new EngineAction<Cursor<T>>(){ @Override public Cursor<T> act(Engine engine) { return new ConvertingCursor(engine.queryTable(query)); } }); } @Override public List<T> findAll(XPathQuery query) { List<T> ret = new ArrayList<>(); try(Cursor<Element> data = this.queryTable(query)){ for(Element e : data){ ret.add(convert(e)); } } return ret; } @Override public void replace(T newValue) throws KeyNotFoundException { final String id = getId(newValue); if(id == null){ throw new KeyNotFoundException("Object has no ID"); } final Element data = convert(newValue, id); this.doWithEngine(new EngineActionEx<Object, KeyNotFoundException>(){ @Override public Object act(Engine engine) throws KeyNotFoundException { engine.replaceRow(id, data); return null; } }); } @Override public boolean replaceOne(XPathQuery query, T newValue) { Element existing = this.findOneElement(query); if(existing == null){ return false; } Element data = convert(newValue, null); String replacedId = recursiveReplaceOne(query, data, existing); if(replacedId == null){ return false; } try { this.accessor.setIdValue(newValue, this.getIdGenerator().stringToId(replacedId, this.accessor.getIdType())); } catch (IllegalAccessException | InvocationTargetException ex) { throw new XFlatException("Unable to update object ID", ex); } return true; } private String recursiveReplaceOne(XPathQuery query, final Element data, Element existing){ if(existing == null){ return null; } final String id = getId(existing); setId(data, id); try{ this.doWithEngine(new EngineActionEx<Object, KeyNotFoundException>(){ @Override public Object act(Engine engine) throws KeyNotFoundException { engine.replaceRow(id, data); return null; } }); return id; } catch(KeyNotFoundException ex){ //concurrent modification, try again existing = this.findOneElement(query); return recursiveReplaceOne(query, data, existing); } } @Override public boolean upsert(T newValue) { final String id = getOrGenerateId(newValue); final Element data = convert(newValue, id); return this.doWithEngine(new EngineAction<Boolean>(){ @Override public Boolean act(Engine engine) { return engine.upsertRow(id, data); } }); } @Override public boolean update(Object id, final XPathUpdate update) throws KeyNotFoundException { if(id == null){ throw new IllegalArgumentException("Id cannot be null"); } final String sId = this.getIdGenerator().idToString(id); return this.doWithEngine(new EngineActionEx<Boolean, KeyNotFoundException>(){ @Override public Boolean act(Engine engine) throws KeyNotFoundException { return engine.update(sId, update); } }); } @Override public int update(final XPathQuery query, final XPathUpdate update) { query.setAlternateIdExpression(alternateIdExpression); return this.doWithEngine(new EngineAction<Integer>(){ @Override public Integer act(Engine engine) { return engine.update(query, update); } }); } @Override public void delete(Object id) throws KeyNotFoundException { if(id == null){ throw new IllegalArgumentException("id cannot be null"); } final String sId = this.getIdGenerator().idToString(id); this.doWithEngine(new EngineActionEx<Object, KeyNotFoundException>(){ @Override public Object act(Engine engine) throws KeyNotFoundException { engine.deleteRow(sId); return null; } }); } @Override public int deleteAll(final XPathQuery query) { query.setAlternateIdExpression(alternateIdExpression); return this.doWithEngine(new EngineAction<Integer>(){ @Override public Integer act(Engine engine) { return engine.deleteAll(query); } }); } private class ConvertingCursor implements Cursor<T>{ Cursor<Element> rowCursor; public ConvertingCursor(Cursor<Element> rowCursor){ this.rowCursor = rowCursor; } @Override public Iterator<T> iterator() { return new ConvertingCursorIterator(this.rowCursor.iterator()); } @Override public void close() throws XFlatException { this.rowCursor.close(); } } private class ConvertingCursorIterator implements Iterator<T>{ Iterator<Element> rowIterator; public ConvertingCursorIterator(Iterator<Element> rowIterator){ this.rowIterator = rowIterator; } @Override public boolean hasNext() { return rowIterator.hasNext(); } @Override public T next() { return convert(rowIterator.next()); } @Override public void remove() { throw new UnsupportedOperationException("Remove not supported on cursors."); } } }