org.xflatdb.xflat.db.ConvertingTable.java Source code

Java tutorial

Introduction

Here is the source code for org.xflatdb.xflat.db.ConvertingTable.java

Source

/* 
*   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.");
        }
    }
}