/*
* Copyright (c) 2009, SQL Power Group Inc.
*
* This file is part of SQL Power Library.
*
* SQL Power Library is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* SQL Power Library 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 Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package ca.sqlpower.dao;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.imageio.ImageIO;
import com.google.common.collect.Multimap;
import ca.sqlpower.dao.SPPersister.DataType;
import ca.sqlpower.dao.session.BidirectionalConverter;
import ca.sqlpower.dao.session.SessionPersisterSuperConverter;
import ca.sqlpower.object.SPObject;
import ca.sqlpower.object.annotation.Accessor;
import ca.sqlpower.sqlobject.SQLObject;
/**
* Utilities that are used by {@link SPPersister}s.
*/
public class PersisterUtils {
private PersisterUtils() {
//cannot instantiate this class as it is just static utility methods.
}
/**
* Converts an image to an output stream to be persisted in some way.
*
* @param img
* The image to convert to an output stream for persisting.
* @return An output stream containing an encoding of the image as PNG.
*/
public static ByteArrayOutputStream convertImageToStreamAsPNG(Image img) {
BufferedImage image;
if (img instanceof BufferedImage) {
image = (BufferedImage) img;
} else {
image = new BufferedImage(img.getWidth(null),
img.getHeight(null), BufferedImage.TYPE_INT_ARGB);
final Graphics2D g = image.createGraphics();
g.drawImage(img, 0, 0, null);
g.dispose();
}
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
if (image != null) {
try {
ImageIO.write(image, "PNG", byteStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return byteStream;
}
/**
* Splits a string by the converter delimiter and checks that the correct
* number of pieces are returned or it throws an
* {@link IllegalArgumentException}. This is a simple place to do general
* error checking when first converting a string into an object.
*
* @param toSplit
* The string to split by the delimiter.
* @param numPieces
* The number of pieces the string must be split into.
* @return The pieces the string is split into.
*/
public static String[] splitByDelimiter(String toSplit, int numPieces) {
String[] pieces = toSplit.split(BidirectionalConverter.DELIMITER);
if (pieces.length > numPieces) {
throw new IllegalArgumentException("Cannot convert string \""
+ toSplit + "\" with an invalid number of properties.");
} else if (pieces.length < numPieces) {
//split will strip off empty space that comes after a delimiter instead of
//appending an empty string to the array so we have to do that ourselves.
String[] allPieces = new String[numPieces];
int i = 0;
for (String piece : pieces) {
allPieces[i] = piece;
i++;
}
for (; i < numPieces; i++) {
allPieces[i] = "";
}
return allPieces;
}
return pieces;
}
/**
* Returns a set of all the interesting property names of the given SQLObject type.
* @param type
* @return
* @throws SecurityException
* @throws IllegalArgumentException
* @throws ClassNotFoundException
* @throws IllegalAccessException
* @throws InvocationTargetException
*/
public static Set<String> getInterestingPropertyNames(String type)
throws SecurityException, IllegalArgumentException, ClassNotFoundException, IllegalAccessException, InvocationTargetException {
return getInterestingProperties(type, null, null).keySet();
}
/**
* Returns a map containing all the interesting properties of the class type
* given by the fully qualified name. An interesting property is a
* non-transient accessor with the isInteresting flag set to true. The
* properties are mapped by their name, and contain their value, converted
* to a non-complex type
*
* @param className
* @param converter
* @return
* @throws SecurityException
* @throws ClassNotFoundException
* @throws InvocationTargetException
* @throws IllegalAccessException
* @throws IllegalArgumentException
*/
public static Map<String, Object> getInterestingProperties(SQLObject object, SessionPersisterSuperConverter converter)
throws SecurityException, ClassNotFoundException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
return getInterestingProperties(object.getClass().getName(), object, converter);
}
/**
* Does what the other getInterestingProperties says it does if passed a non-null object
* Otherwise, it will return a map with a key set of all the property names, but no values.
*
* @param type
* @param object
* @param converter
* @return
* @throws SecurityException
* @throws ClassNotFoundException
* @throws IllegalArgumentException
* @throws IllegalAccessException
* @throws InvocationTargetException
*/
private static Map<String, Object> getInterestingProperties(
String type, SQLObject object, SessionPersisterSuperConverter converter)
throws SecurityException, ClassNotFoundException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
Map<String, Object> propertyMap = new HashMap<String, Object>();
Class<? extends Object> objectClass = Class.forName(type, true, PersisterUtils.class.getClassLoader());
for (Method m : objectClass.getMethods()) {
if (m.getAnnotation(Accessor.class) != null
&& m.getAnnotation(Accessor.class).isInteresting()) {
String propertyName;
if (m.getName().startsWith("get")) {
propertyName = m.getName().substring(3);
} else if (m.getName().startsWith("is")) {
propertyName = m.getName().substring(2);
} else {
throw new RuntimeException("Accessor class with improper prefix");
}
String firstCharacter = String.valueOf(propertyName.charAt(0));
propertyName = propertyName.replaceFirst(
firstCharacter, firstCharacter.toLowerCase());
if (object != null) {
propertyMap.put(propertyName,
converter.convertToBasicType(m.invoke(object)));
} else {
propertyMap.put(propertyName, "");
}
}
}
return propertyMap;
}
/**
* Gets the correct data type based on the given class for the {@link SPPersister}.
*/
public static DataType getDataType(Class<? extends Object> classForDataType) {
if (classForDataType == null) return DataType.NULL;
if (Integer.class.isAssignableFrom(classForDataType)) {
return DataType.INTEGER;
} else if (Long.class.isAssignableFrom(classForDataType)) {
return DataType.LONG;
} else if (Short.class.isAssignableFrom(classForDataType)) {
return DataType.SHORT;
} else if (Boolean.class.isAssignableFrom(classForDataType)) {
return DataType.BOOLEAN;
} else if (Double.class.isAssignableFrom(classForDataType)) {
return DataType.DOUBLE;
} else if (Float.class.isAssignableFrom(classForDataType)) {
return DataType.FLOAT;
} else if (String.class.isAssignableFrom(classForDataType)) {
return DataType.STRING;
} else if (Image.class.isAssignableFrom(classForDataType)) {
return DataType.PNG_IMG;
} else if (SPObject.class.isAssignableFrom(classForDataType)) {
return DataType.REFERENCE;
} else if (Void.class.isAssignableFrom(classForDataType)) {
return DataType.NULL;
} else {
return DataType.STRING;
}
}
/**
* Produces and int array from a String containing comma-separated values
* @param data A String containing comma-separated values, eg: "-1, 7, 2", "-1,7,2", "-1", ""
* @return {-1, 7, 2}, {-1, 7, 2}, {-1}, {}
*/
public static int[] convertStringToIntArray(String data) {
String[] s = data.split(",");
int[] ints = new int[s.length];
for (int i = 0; i < s.length; i++) {
try {
ints[i] = Integer.parseInt(s[i]);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(e);
}
}
return ints;
}
/**
* Produces a String containing the given integers, separated by commas.
* @param data An int array, eg: {-1, 7, 2}, {-1}, {}
* @return A String of comma-separated values, eg: "-1,7,2", "-1", ""
*/
public static String convertIntArrayToString(int[] data) {
String s = "";
for (int i = 0; i < data.length; i++) {
s += String.valueOf(data[i]);
if (i != data.length - 1) {
s += ",";
}
}
return s;
}
/**
* Returns the first index of this childType in the child type list of
* the parentType. If a superclass or interface of the childType exists
* in the list of acceptable child types of the parent this index may be
* returned, depending if it is the first. If the childType is not a
* valid child type of the parentType -1 will be returned.
*/
@SuppressWarnings("unchecked")
public static int getTypePosition(String childClassName, String parentClassName)
throws IllegalArgumentException, SecurityException, IllegalAccessException, NoSuchFieldException, ClassNotFoundException {
Class<? extends SPObject> childType = (Class<? extends SPObject>) PersisterUtils.class.getClassLoader().loadClass(childClassName);
Class<? extends SPObject> parentType = (Class<? extends SPObject>) PersisterUtils.class.getClassLoader().loadClass(parentClassName);
List<Class<? extends SPObject>> allowedChildTypes = (List<Class<? extends SPObject>>)
parentType.getDeclaredField("allowedChildTypes").get(null);
for (int i = 0; i < allowedChildTypes.size(); i++) {
Class<? extends SPObject> allowedType = allowedChildTypes.get(i);
if (allowedType.isAssignableFrom(childType)) {
return i;
}
}
return -1;
}
/**
* Returns the class type that the given parent class allows that is either
* the same or a superclass or interface of the given child class. Returns
* the child class if the parent class is null or the empty string. Returns
* null if the parent class has no valid child type of the given child
* class.
*/
@SuppressWarnings("unchecked")
public static Class<? extends SPObject> getParentAllowedChildType(String childClassName, String parentClassName)
throws IllegalArgumentException, SecurityException, IllegalAccessException, NoSuchFieldException, ClassNotFoundException {
Class<? extends SPObject> childType = (Class<? extends SPObject>) PersisterUtils.class.getClassLoader().loadClass(childClassName);
if (parentClassName == null || parentClassName.trim().length() == 0) {
return childType;
}
Class<? extends SPObject> parentType = (Class<? extends SPObject>) PersisterUtils.class.getClassLoader().loadClass(parentClassName);
return getParentAllowedChildType(childType, parentType);
}
@SuppressWarnings("unchecked")
public static Class<? extends SPObject> getParentAllowedChildType(
Class<? extends SPObject> childType,
Class<? extends SPObject> parentType)
throws IllegalAccessException, NoSuchFieldException {
List<Class<? extends SPObject>> allowedChildTypes = (List<Class<? extends SPObject>>)
parentType.getDeclaredField("allowedChildTypes").get(null);
if (allowedChildTypes.contains(childType)) return childType;
for (int i = 0; i < allowedChildTypes.size(); i++) {
Class<? extends SPObject> allowedType = allowedChildTypes.get(i);
if (allowedType.isAssignableFrom(childType)) {
return allowedType;
}
}
return null;
}
/**
* A way to get the allowed child list from a class object that is an SPObject.
*/
@SuppressWarnings("unchecked")
public static List<Class<? extends SPObject>> getAllowedChildTypes(Class<? extends SPObject> parentClass)
throws IllegalArgumentException, SecurityException, IllegalAccessException, NoSuchFieldException {
return (List<Class<? extends SPObject>>) parentClass.getDeclaredField("allowedChildTypes").get(null);
}
/**
* Returns true if there is a boolean property persisted on the given UUID
* with the given property name and the property is being set to true.
* Returns false if there is a boolean property persisted on the given UUID
* with the given property name and the property is being set to false.
* Returns null if the persist call does not exist.
*
* @param persistedProperties
* The list of properties that have been persisted to search for
* the persist call.
* @param parentUUID
* The UUID of the object we are looking for the boolean property
* of.
* @param propName
* The property name that describes a boolean property. The
* property if it exists must represent a boolean.
* @return
*/
public static Boolean findPersistedBooleanProperty(
Multimap<String, PersistedSPOProperty> persistedProperties,
String parentUUID, String propName) {
Collection<PersistedSPOProperty> properties = persistedProperties.get(parentUUID);
if (properties == null || properties.isEmpty()) return null;
for (PersistedSPOProperty property : properties) {
if (property.getPropertyName().equals(propName)) {
return (Boolean) property.getNewValue();
}
}
return null;
}
/**
* Returns a new {@link PersistedSPObject} based on a given {@link SPObject}.
*/
public static PersistedSPObject createPersistedObjectFromSPObject(SPObject spo) {
String parentUUID = null;
int index = 0;
SPObject parent = spo.getParent();
if (parent != null) {
parentUUID = parent.getUUID();
List<? extends SPObject> siblings;
Class<? extends SPObject> siblingClass;
try {
siblingClass = PersisterUtils.getParentAllowedChildType(spo.getClass().getName(), parent.getClass().getName());
} catch (Exception e) {
throw new RuntimeException(e);
}
if (parent instanceof SQLObject) {
siblings = ((SQLObject) parent).getChildrenWithoutPopulating(siblingClass);
} else {
siblings = parent.getChildren(siblingClass);
}
index = siblings.indexOf(spo);
}
return new PersistedSPObject(parentUUID, spo.getClass().getName(),
spo.getUUID(), index);
}
}