package communitycommons; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import com.mendix.core.Core; import com.mendix.core.CoreException; import com.mendix.systemwideinterfaces.core.IContext; import com.mendix.systemwideinterfaces.core.IMendixIdentifier; import com.mendix.systemwideinterfaces.core.IMendixObject; public class XPath<T> { /** Build in tokens, see: https://world.mendix.com/display/refguide3/XPath+Keywords+and+System+Variables * */ public static final String CurrentUser = "[%CurrentUser%]"; public static final String CurrentObject = "[%CurrentObject%]"; public static final String CurrentDateTime = "[%CurrentDateTime%]"; public static final String BeginOfCurrentDay = "[%BeginOfCurrentDay%]"; public static final String EndOfCurrentDay = "[%EndOfCurrentDay%]"; public static final String BeginOfCurrentHour = "[%BeginOfCurrentHour%]"; public static final String EndOfCurrentHour = "[%EndOfCurrentHour%]"; public static final String BeginOfCurrentMinute = "[%BeginOfCurrentMinute%]"; public static final String EndOfCurrentMinute = "[%EndOfCurrentMinute%]"; public static final String BeginOfCurrentMonth = "[%BeginOfCurrentMonth%]"; public static final String EndOfCurrentMonth = "[%EndOfCurrentMonth%]"; public static final String BeginOfCurrentWeek = "[%BeginOfCurrentWeek%]"; public static final String EndOfCurrentWeek = "[%EndOfCurrentWeek%]"; public static final String DayLength = "[%DayLength%]"; public static final String HourLength = "[%HourLength%]"; public static final String MinuteLength = "[%MinuteLength%]"; public static final String SecondLength = "[%SecondLength%]"; public static final String WeekLength = "[%WeekLength%]"; public static final String YearLength = "[%YearLength%]"; public static final String ID = "id"; /** End builtin tokens */ private String entity; private int offset = 0; private int limit = -1; private LinkedHashMap<String, String> sorting = new LinkedHashMap<String, String>(); //important, linked map! private LinkedList<String> closeStack = new LinkedList<String>(); private StringBuffer builder = new StringBuffer(); private IContext context; private Class<T> proxyClass; private boolean requiresBinOp = false; //state property, indicates whether and 'and' needs to be inserted before the next constraint public static XPath<IMendixObject> create(IContext c, String entityType) { XPath<IMendixObject> res = new XPath<IMendixObject>(c, IMendixObject.class); res.entity = entityType; return res; } public static <U> XPath<U> create(IContext c, Class<U> proxyClass) { return new XPath<U>(c, proxyClass); } private XPath(IContext c, Class<T> proxyClass) { try { if (proxyClass != IMendixObject.class) this.entity = (String) proxyClass.getMethod("getType").invoke(null); } catch (Exception e) { throw new IllegalArgumentException("Failed to determine entity type of proxy class. Did you provide a valid proxy class? '" + proxyClass.getName() + "'"); } this.proxyClass = proxyClass; this.context = c; } private XPath<T> autoInsertAnd() { if (requiresBinOp) and(); return this; } private XPath<T> requireBinOp(boolean requires) { requiresBinOp = requires; return this; } public XPath<T> offset(int offset2) { if (offset2 < 0) throw new IllegalArgumentException("Offset should not be negative"); this.offset = offset2; return this; } public XPath<T> limit(int limit2) { if (limit2 < -1 || limit2 == 0) throw new IllegalArgumentException("Limit should be larger than zero or -1. "); this.limit = limit2; return this; } public XPath<T> addSortingAsc(Object... sortparts) { assertOdd(sortparts); sorting.put(StringUtils.join(sortparts, '/'), "asc"); return this; } public XPath<T> addSortingDesc(Object... sortparts) { sorting.put(StringUtils.join(sortparts, "/"), "desc"); return this; } public XPath<T> eq(Object attr, Object valuecomparison) { return compare(attr, "=", valuecomparison); } public XPath<T> eq(Object... pathAndValue) { assertEven(pathAndValue); return compare(Arrays.copyOfRange(pathAndValue, 0, pathAndValue.length -1), "=", pathAndValue[pathAndValue.length -1 ]); } public XPath<T> equalsIgnoreCase(Object attr, String value) { //(contains(Name, $email) and length(Name) = length($email) return subconstraint() .contains(attr, value) .and() .append(" length(" + attr + ") = ").append(value == null ? "0" : valueToXPathValue(value.length())) .close(); } public XPath<T> notEq(Object attr, Object valuecomparison) { return compare(attr, "!=", valuecomparison); } public XPath<T> notEq(Object... pathAndValue) { assertEven(pathAndValue); return compare(Arrays.copyOfRange(pathAndValue, 0, pathAndValue.length -1), "!=", pathAndValue[pathAndValue.length -1 ]); } public XPath<T> contains(Object attr, String value) { autoInsertAnd().append(" contains(").append(String.valueOf(attr)).append(",").append(valueToXPathValue(value)).append(") "); return this.requireBinOp(true); } public XPath<T> compare(Object attr, String operator, Object value) { return compare(new Object[] {attr}, operator, value); } public XPath<T> compare(Object[] path, String operator, Object value) { assertOdd(path); autoInsertAnd().append(StringUtils.join(path, '/')).append(" ").append(operator).append(" ").append(valueToXPathValue(value)); return this.requireBinOp(true); } public XPath<T> hasReference(Object... path) { assertEven(path); //Reference + entity type autoInsertAnd().append(StringUtils.join(path, '/')); return this.requireBinOp(true); } public XPath<T> subconstraint(Object... path) { assertEven(path); autoInsertAnd().append(StringUtils.join(path, '/')).append("["); closeStack.push("]"); return this.requireBinOp(false); } public XPath<T> subconstraint() { autoInsertAnd().append("("); closeStack.push(")"); return this.requireBinOp(false); } public XPath<T> addConstraint() { if (!closeStack.isEmpty() && !closeStack.peek().equals("]")) throw new IllegalStateException("Cannot add a constraint while in the middle of something else.."); return append("][").requireBinOp(false); } public XPath<T> close() { if (closeStack.isEmpty()) throw new IllegalStateException("XPathbuilder close stack is empty!"); append(closeStack.pop()); return requireBinOp(true); //MWE: note that a close does not necessary require a binary operator, for example with two subsequent block([bla][boe]) constraints, //but openening a binary constraint reset the flag, so that should be no issue } public XPath<T> or() { if (!requiresBinOp) throw new IllegalStateException("Received 'or' but no binary operator was expected"); return append(" or ").requireBinOp(false); } public XPath<T> and() { if (!requiresBinOp) throw new IllegalStateException("Received 'and' but no binary operator was expected"); return append(" and ").requireBinOp(false); } public XPath<T> not() { autoInsertAnd(); closeStack.push(")"); return append(" not(").requireBinOp(false); } private void assertOdd(Object[] stuff) { if (stuff == null || stuff.length == 0 || stuff.length % 2 == 0) throw new IllegalArgumentException("Expected an odd number of xpath path parts"); } private void assertEven(Object[] stuff) { if (stuff == null || stuff.length == 0 || stuff.length % 2 == 1) throw new IllegalArgumentException("Expected an even number of xpath path parts"); } public XPath<T> append(String s) { builder.append(s); return this; } public String getXPath() { if (builder.length() > 0) return "//" + this.entity + "[" + builder.toString() + "]"; return "//" + this.entity; } private void assertEmptyStack() throws IllegalStateException { if (!closeStack.isEmpty()) throw new IllegalStateException("Invalid xpath expression, not all items where closed"); } public long count( ) throws CoreException { assertEmptyStack(); return Core.retrieveXPathQueryAggregate(context, "count(" + getXPath() +")"); } public IMendixObject firstMendixObject() throws CoreException { assertEmptyStack(); List<IMendixObject> result = Core.retrieveXPathQuery(context, getXPath(), 1, offset, sorting); if (result.isEmpty()) return null; return result.get(0); } public T first() throws CoreException { return createProxy(context, proxyClass, firstMendixObject()); } /** * Given a set of attribute names and values, tries to find the first object that matches all conditions, or creates one * * @param autoCommit: whether the object should be committed once created (default: true) * @param keysAndValues * @return * @throws CoreException */ public T findOrCreateNoCommit(Object... keysAndValues) throws CoreException { T res = findFirst(keysAndValues); return res != null ? res : constructInstance(false, keysAndValues); } public T findOrCreate(Object... keysAndValues) throws CoreException { T res = findFirst(keysAndValues); return res != null ? res : constructInstance(true, keysAndValues); } public T findOrCreateSynchronized(Object... keysAndValues) throws CoreException, InterruptedException { T res = findFirst(keysAndValues); if (res != null) { return res; } else { synchronized (Core.getMetaObject(entity)) { IContext synchronizedContext = context.getSession().createContext().createSudoClone(); try { synchronizedContext.startTransaction(); res = createProxy(synchronizedContext, proxyClass, XPath.create(synchronizedContext, entity).findOrCreate(keysAndValues)); synchronizedContext.endTransaction(); return res; } catch (CoreException e) { if (synchronizedContext.isInTransaction()) { synchronizedContext.rollbackTransAction(); } throw e; } } } } public T findFirst(Object... keysAndValues) throws IllegalStateException, CoreException { if (builder.length() > 0) throw new IllegalStateException("FindFirst can only be used on XPath which do not have constraints already"); assertEven(keysAndValues); for(int i = 0; i < keysAndValues.length; i+= 2) eq(keysAndValues[i], keysAndValues[i + 1]); T res = this.first(); return res; } /** * Creates one instance of the type of this XPath query, and initializes the provided attributes to the provided values. * @param keysAndValues AttributeName, AttributeValue, AttributeName2, AttributeValue2... list. * @return * @throws CoreException */ public T constructInstance(boolean autoCommit, Object... keysAndValues) throws CoreException { assertEven(keysAndValues); IMendixObject newObj = Core.instantiate(context, this.entity); for(int i = 0; i < keysAndValues.length; i+= 2) newObj.setValue(context, String.valueOf(keysAndValues[i]), toMemberValue(keysAndValues[i + 1])); if (autoCommit) Core.commit(context, newObj); return createProxy(context, proxyClass, newObj); } /** * Given a current collection of primitive values, checks if for each value in the collection an object in the database exists. * It creates a new object if needed, and removes any superfluos objects in the database that are no longer in the collection. * * @param currentCollection The collection that act as reference for the objects that should be in this database in the end. * @param comparisonAttribute The attribute that should store the value as decribed in the collection * @param autoDelete Automatically remove any superfluous objects form the database * @param keysAndValues Constraints that should hold for the set of objects that are deleted or created. Objects outside this constraint are not processed. * * @return A pair of lists. The first list contains the newly created objects, the second list contains the objects that (should be or are) removed. * @throws CoreException */ public <U> ImmutablePair<List<T>, List<T>> syncDatabaseWithCollection(Collection<U> currentCollection, Object comparisonAttribute, boolean autoDelete, Object... keysAndValues) throws CoreException { if (builder.length() > 0) throw new IllegalStateException("syncDatabaseWithCollection can only be used on XPath which do not have constraints already"); List<T> added = new ArrayList<T>(); List<T> removed = new ArrayList<T>(); Set<U> col = new HashSet<U>(currentCollection); for(int i = 0; i < keysAndValues.length; i+= 2) eq(keysAndValues[i], keysAndValues[i + 1]); for(IMendixObject existingItem : this.allMendixObjects()) { //Item is still available if (col.remove(existingItem.getValue(context, String.valueOf(comparisonAttribute)))) continue; //No longer available removed.add(createProxy(context, this.proxyClass, existingItem)); if (autoDelete) Core.delete(context, existingItem); } //Some items where not found in the database for(U value : col) { //In apache lang3, this would just be: ArrayUtils.addAll(keysAndValues, comparisonAttribute, value) Object[] args = new Object[keysAndValues.length + 2]; for(int i = 0; i < keysAndValues.length; i++) args[i] = keysAndValues[i]; args[keysAndValues.length] = comparisonAttribute; args[keysAndValues.length + 1] = value; T newItem = constructInstance(true, args); added.add(newItem); } //Oké, stupid, Pair is also only available in apache lang3, so lets use a simple pair implementation for now return ImmutablePair.of(added, removed); } public T firstOrWait(long timeoutMSecs) throws CoreException, InterruptedException { IMendixObject result = null; long start = System.currentTimeMillis(); int sleepamount = 200; int loopcount = 0; while (result == null) { loopcount += 1; result = firstMendixObject(); long now = System.currentTimeMillis(); if (start + timeoutMSecs < now) //Time expired break; if (loopcount % 5 == 0) sleepamount *= 1.5; //not expired, wait a bit if (result == null) Thread.sleep(sleepamount); } return createProxy(context, proxyClass, result); } public List<IMendixObject> allMendixObjects() throws CoreException { assertEmptyStack(); return Core.retrieveXPathQuery(context, getXPath(), limit, offset, sorting); } public List<T> all() throws CoreException { List<T> res = new ArrayList<T>(); for(IMendixObject o : allMendixObjects()) res.add(createProxy(context, proxyClass, o)); return res; } @Override public String toString() { return getXPath(); } /** * * * Static utility functions * * */ //cache for proxy constructors. Reflection is slow, so reuse as much as possible private static Map<String, Method> initializers = new HashMap<String, Method>(); public static <T> List<T> createProxyList(IContext c, Class<T> proxieClass, List<IMendixObject> objects) { List<T> res = new ArrayList<T>(); if (objects == null || objects.size() == 0) return res; for(IMendixObject o : objects) res.add(createProxy(c, proxieClass, o)); return res; } public static <T> T createProxy(IContext c, Class<T> proxieClass, IMendixObject object) { //Borrowed from nl.mweststrate.pages.MxQ package if (object == null) return null; if (c == null || proxieClass == null) throw new IllegalArgumentException("[CreateProxy] No context or proxieClass provided. "); //jeuj, we expect IMendixObject's. Thats nice.. if (proxieClass == IMendixObject.class) return proxieClass.cast(object); //.. since we can do a direct cast try { String entityType = object.getType(); if (!initializers.containsKey(entityType)) { String[] entType = object.getType().split("\\."); Class<?> realClass = Class.forName(entType[0].toLowerCase()+".proxies."+entType[1]); initializers.put(entityType, realClass.getMethod("initialize", IContext.class, IMendixObject.class)); } //find constructor Method m = initializers.get(entityType); //create proxy object Object result = m.invoke(null, c, object); //cast, but check first is needed because the actual type might be a subclass of the requested type if (!proxieClass.isAssignableFrom(result.getClass())) throw new IllegalArgumentException("The type of the object ('" + object.getType() + "') is not (a subclass) of '" + proxieClass.getName()+"'"); T proxie = proxieClass.cast(result); return proxie; } catch (Exception e) { throw new RuntimeException("Unable to instantiate proxie: " + e.getMessage(), e); } } public static String valueToXPathValue(Object value) { if (value == null) return "NULL"; //Complex objects if (value instanceof IMendixIdentifier) return "'" + String.valueOf(((IMendixIdentifier) value).toLong()) + "'"; if (value instanceof IMendixObject) return valueToXPathValue(((IMendixObject)value).getId()); if (value instanceof List<?>) throw new IllegalArgumentException("List based values are not supported!"); //Primitives if (value instanceof Date) return String.valueOf(((Date) value).getTime()); if (value instanceof Long || value instanceof Integer) return String.valueOf(value); if (value instanceof Double || value instanceof Float) { //make sure xpath understands our number formatting NumberFormat format = NumberFormat.getNumberInstance(Locale.ENGLISH); format.setMaximumFractionDigits(10); format.setGroupingUsed(false); return format.format(value); } if (value instanceof Boolean) { return value.toString() + "()"; //xpath boolean, you know.. } if (value instanceof String) { return "'" + StringEscapeUtils.escapeXml(String.valueOf(value)) + "'"; } //Object, assume its a proxy and deproxiefy try { IMendixObject mo = proxyToMendixObject(value); return valueToXPathValue(mo); } catch (NoSuchMethodException e) { //This is O.K. just not a proxy object... } catch (Exception e) { throw new RuntimeException("Failed to retrieve MendixObject from proxy: " + e.getMessage(), e); } //assume some string representation return "'" + StringEscapeUtils.escapeXml(String.valueOf(value)) + "'"; } public static IMendixObject proxyToMendixObject(Object value) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { Method m = value.getClass().getMethod("getMendixObject"); IMendixObject mo = (IMendixObject) m.invoke(value); return mo; } public static <T> List<IMendixObject> proxyListToMendixObjectList( List<T> objects) throws SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { ArrayList<IMendixObject> res = new ArrayList<IMendixObject>(objects.size()); for(T i : objects) res.add(proxyToMendixObject(i)); return res; } public static Object toMemberValue(Object value) { if (value == null) return null; //Complex objects if (value instanceof IMendixIdentifier) return value; if (value instanceof IMendixObject) return ((IMendixObject)value).getId(); if (value instanceof List<?>) throw new IllegalArgumentException("List based values are not supported!"); //Primitives if ( value instanceof Date || value instanceof Long || value instanceof Integer || value instanceof Double || value instanceof Float || value instanceof Boolean || value instanceof String) { return value; } if (value.getClass().isEnum()) return value.toString(); //Object, assume its a proxy and deproxiefy try { Method m = value.getClass().getMethod("getMendixObject"); IMendixObject mo = (IMendixObject) m.invoke(value); return toMemberValue(mo); } catch (NoSuchMethodException e) { //This is O.K. just not a proxy object... } catch (Exception e) { throw new RuntimeException("Failed to convert object to IMendixMember compatible value '" + value + "': " + e.getMessage(), e); } throw new RuntimeException("Failed to convert object to IMendixMember compatible value: " + value); } public static interface IBatchProcessor<T> { public void onItem(T item, long offset, long total) throws Exception; } private static final class ParallelJobRunner<T> implements Callable<Boolean> { private final XPath<T> self; private final IBatchProcessor<T> batchProcessor; private final IMendixObject item; private long index; private long count; ParallelJobRunner(XPath<T> self, IBatchProcessor<T> batchProcessor, IMendixObject item, long index, long count) { this.self = self; this.batchProcessor = batchProcessor; this.item = item; this.index = index; this.count = count; } @Override public Boolean call() { try { batchProcessor.onItem(XPath.createProxy(Core.createSystemContext(), self.proxyClass, item), index, count); //mwe: hmm, many contexts.. return true; } catch (Exception e) { throw new RuntimeException(String.format("Failed to execute batch on '%s' offset %d: %s", self.toString(), self.offset, e.getMessage()), e); } } } /** * Retreives all items in this xpath query in batches of a limited size. * Not that this function does not start a new transaction for all the batches, * rather, it just limits the number of objects being retrieved and kept in memory at the same time. * * So it only batches the retrieve process, not the optional manipulations done in the onItem method. * @param batchsize * @param batchProcessor * @throws CoreException */ public void batch(int batchsize, IBatchProcessor<T> batchProcessor) throws CoreException { if (this.sorting.isEmpty()) this.addSortingAsc(XPath.ID); long count = this.count(); int baseoffset = this.offset; int baselimit = this.limit; boolean useBaseLimit = baselimit > -1; this.offset(baseoffset); List<T> data; long i = 0; do { int newlimit = useBaseLimit ? Math.min(batchsize, baseoffset + baselimit - this.offset) : batchsize; if (newlimit == 0) break; //where done, no more data is needed this.limit(newlimit); data = this.all(); for(T item : data) { i += 1; try { batchProcessor.onItem(item, i, Math.max(i, count)); } catch (Exception e) { throw new RuntimeException(String.format("Failed to execute batch on '%s' offset %d: %s", this.toString(), this.offset, e.getMessage()), e); } } this.offset(this.offset + data.size()); } while(data.size() > 0); } /** * Batch with parallelization. * * IMPORTANT NOTE: DO NOT USE THE CONTEXT OF THE XPATH OBJECT ITSELF INSIDE THE BATCH PROCESSOR! * * Instead, use: Item.getContext(); !! * * * @param batchsize * @param threads * @param batchProcessor * @throws CoreException * @throws InterruptedException * @throws ExecutionException */ public void batch(int batchsize, int threads, final IBatchProcessor<T> batchProcessor) throws CoreException, InterruptedException, ExecutionException { if (this.sorting.isEmpty()) this.addSortingAsc(XPath.ID); ExecutorService pool = Executors.newFixedThreadPool(threads); final long count = this.count(); final XPath<T> self = this; int progress = 0; List<Future<?>> futures = new ArrayList<Future<?>>(batchsize); //no need to synchronize this.offset(0); this.limit(batchsize); List<IMendixObject> data = this.allMendixObjects(); while (data.size() > 0) { for (final IMendixObject item : data) { futures.add(pool.submit(new ParallelJobRunner<T>(self, batchProcessor, item, progress, count))); progress += 1; } while (!futures.isEmpty()) futures.remove(0).get(); //wait for all futures before proceeding to next iteration this.offset(this.offset + data.size()); data = this.allMendixObjects(); } if (pool.shutdownNow().size() > 0) throw new IllegalStateException("Not all tasks where finished!"); } public static Class<?> getProxyClassForEntityName(String entityname) { { String [] parts = entityname.split("\\."); try { return Class.forName(parts[0].toLowerCase() + ".proxies." + parts[1]); } catch (ClassNotFoundException e) { throw new RuntimeException("Cannot find class for entity: " + entityname + ": " + e.getMessage(), e); } } } public boolean deleteAll() throws CoreException { this.limit(1000); List<IMendixObject> objs = allMendixObjects(); while (!objs.isEmpty()) { if (!Core.delete(context, objs.toArray(new IMendixObject[objs.size()]))) return false; //TODO: throw? objs = allMendixObjects(); } return true; } }