diff --git a/LICENSE_APACHEv2.txt b/LICENSE_APACHEv2.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE_APACHEv2.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f315fcc --- /dev/null +++ b/pom.xml @@ -0,0 +1,51 @@ + + 4.0.0 + jmockmongo + jmockmongo + 0.0.1-SNAPSHOT + Mocking MongoDB + Disturbingly incomplete in-memory MongoDB implementation for use in automated testing + + + Apache License Version 2.0 + + + 2012 + + + + + com.mycila.maven-license-plugin + maven-license-plugin + + ${basedir}/src +
${basedir}/src/license_header_apachev2.txt
+ true +
+
+ +
+
+ + + org.mongodb + mongo-java-driver + 2.7.3 + jar + compile + + + junit + junit + 3.8.2 + jar + test + + + io.netty + netty + 3.3.1.Final + compile + + +
\ No newline at end of file diff --git a/src/license_header_apachev2.txt b/src/license_header_apachev2.txt new file mode 100644 index 0000000..71f9095 --- /dev/null +++ b/src/license_header_apachev2.txt @@ -0,0 +1,15 @@ +Copyright (c) 2012, Thilo Planz. All rights reserved. + +This program is free software: you can redistribute it and/or modify +it under the terms of the Apache License, Version 2.0 +as published by the Apache Software Foundation (the "License"). + +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. + +You should have received a copy of the License along with this program. +If not, see . diff --git a/src/main/java/jmockmongo/BSONUtils.java b/src/main/java/jmockmongo/BSONUtils.java new file mode 100644 index 0000000..79d9531 --- /dev/null +++ b/src/main/java/jmockmongo/BSONUtils.java @@ -0,0 +1,268 @@ +/** + * Copyright (c) 2012, Thilo Planz. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License, Version 2.0 + * as published by the Apache Software Foundation (the "License"). + * + * 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. + * + * You should have received a copy of the License along with this program. + * If not, see . + */ + +package jmockmongo; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.bson.BSON; +import org.bson.BSONObject; + +/** + * Utility class to work with BSON data. + * + * + */ + +class BSONUtils { + + private static T notNull(T x) { + if (x == null) + return x; + if (x instanceof Map) { + if (((Map) x).isEmpty()) + return null; + return x; + } + + if (x instanceof Object[]) { + if (((Object[]) x).length == 0) + return null; + return x; + } + + if (x instanceof byte[]) { + if (((byte[]) x).length == 0) + return null; + return x; + } + + if (x instanceof Collection) { + if (((Collection) x).isEmpty()) + return null; + return x; + } + + return x; + + } + + static Object get(BSONObject b, String fieldName) { + if (!fieldName.contains(".")) + return notNull(b.get(fieldName)); + String[] path = fieldName.split("\\.", 2); + Object nested = b.get(path[0]); + if (nested == null) + return null; + if (nested instanceof BSONObject) + return get((BSONObject) nested, path[1]); + throw new IllegalArgumentException("cannot get field `" + fieldName + + "` of " + b); + } + + private static Object getRequired(BSONObject b, String fieldName) { + Object x = get(b, fieldName); + if (x == null) + throw new IllegalArgumentException("required field `" + fieldName + + "` is missing in " + b); + return x; + } + + static Long toLong(Object x) { + if (x == null) + return null; + if (x instanceof Long) + return (Long) x; + if (x instanceof Integer) + return Long.valueOf(((Integer) x).intValue()); + if (x instanceof String) + return Long.valueOf((String) x); + throw new IllegalArgumentException("cannot convert `" + x + + "` into a Long"); + } + + static Integer toInteger(Object x) { + if (x == null) + return null; + if (x instanceof Integer) + return (Integer) x; + if (x instanceof Long) + return Integer.valueOf(x.toString()); + if (x instanceof String) + return Integer.valueOf((String) x); + throw new IllegalArgumentException("cannot convert `" + x + + "` into a Long"); + } + + static String toString(Object x) { + if (x == null) + return null; + if (x instanceof String) + return (String) x; + if (x instanceof Number) + return x.toString(); + + throw new IllegalArgumentException("cannot convert `" + x + + "` into a String"); + } + + static Long getLong(BSONObject b, String fieldName) { + return toLong(get(b, fieldName)); + } + + static Integer getInteger(BSONObject b, String fieldName) { + return toInteger(get(b, fieldName)); + } + + static int getRequiredInt(BSONObject b, String fieldName) { + return toInteger(getRequired(b, fieldName)).intValue(); + } + + static long getRequiredLong(BSONObject b, String fieldName) { + return toLong(getRequired(b, fieldName)).longValue(); + } + + static String getString(BSONObject b, String fieldName) { + return toString(get(b, fieldName)); + } + + static BSONObject getObject(BSONObject b, String fieldName) { + Object x = get(b, fieldName); + if (x == null) + return null; + if (x instanceof BSONObject) + return (BSONObject) x; + throw new IllegalArgumentException("cannot convert `" + x + + "` into a BSONObject"); + } + + static Object removeField(BSONObject b, String fieldName) { + if (fieldName.contains(".")) + throw new UnsupportedOperationException("not yet implemented"); + return b.removeField(fieldName); + } + + private static void put(BSONObject b, String fieldName, Object x) { + x = notNull(x); + if (x == null) { + removeField(b, fieldName); + } else { + if (fieldName.contains(".")) + throw new UnsupportedOperationException("not yet implemented"); + b.put(fieldName, x); + } + } + + static Integer putInteger(BSONObject b, String fieldName, Object x) { + Integer i = toInteger(x); + put(b, fieldName, i); + return i; + } + + static Long putLong(BSONObject b, String fieldName, Object x) { + Long l = toLong(x); + put(b, fieldName, l); + return l; + } + + static String putString(BSONObject b, String fieldName, Object x) { + String s = toString(x); + put(b, fieldName, s); + return s; + } + + private static final Long MAX_INT = Long.valueOf(Integer.MAX_VALUE); + private static final Long MIN_INT = Long.valueOf(Integer.MIN_VALUE); + + /** + * stores a number as Integer, if it fits, or as a Long, if not. This saves + * space in the database, but you lose the ability to sort or do range + * queries. + */ + static Number putIntegerOrLong(BSONObject b, String fieldName, Object x) { + if (x instanceof Integer) + return putInteger(b, fieldName, x); + + Long l = toLong(x); + if (l == null) { + removeField(b, fieldName); + return null; + } + if (l.compareTo(MAX_INT) < 0 && l.compareTo(MIN_INT) > 0) { + Integer i = l.intValue(); + b.put(fieldName, i); + return i; + } + b.put(fieldName, l); + return l; + } + + /** + * BSON objects are considered equal when their binary encoding matches + */ + static boolean equals(BSONObject a, BSONObject b) { + return a.keySet().equals(b.keySet()) + && Arrays.equals(BSON.encode(a), BSON.encode(b)); + } + + /** + * @return true, if the field contains (in case of an array) or is equal to + * (in case of a single value) the given BSONObject + */ + static boolean contains(BSONObject b, String fieldName, BSONObject toLookFor) { + Object list = get(b, fieldName); + if (list == null) + return false; + if (list instanceof List) { + for (Object o : (List) list) { + if (o instanceof BSONObject) { + BSONObject x = (BSONObject) o; + if (equals(x, toLookFor)) + return true; + } + } + return false; + } + if (list instanceof Object[]) { + for (Object o : (Object[]) list) { + if (o instanceof BSONObject) { + BSONObject x = (BSONObject) o; + if (equals(x, toLookFor)) + return true; + } + } + return false; + } + if (list instanceof BSONObject) { + BSONObject x = (BSONObject) list; + if (equals(x, toLookFor)) + return true; + } + return false; + + } + +} diff --git a/src/main/java/jmockmongo/CommandHandler.java b/src/main/java/jmockmongo/CommandHandler.java new file mode 100644 index 0000000..952ad08 --- /dev/null +++ b/src/main/java/jmockmongo/CommandHandler.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2012, Thilo Planz. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License, Version 2.0 + * as published by the Apache Software Foundation (the "License"). + * + * 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. + * + * You should have received a copy of the License along with this program. + * If not, see . + */ + +package jmockmongo; + +import org.bson.BSONObject; + +public interface CommandHandler { + + public BSONObject handleCommand(String database, BSONObject command); + +} diff --git a/src/main/java/jmockmongo/DefaultInsertHandler.java b/src/main/java/jmockmongo/DefaultInsertHandler.java new file mode 100644 index 0000000..829f429 --- /dev/null +++ b/src/main/java/jmockmongo/DefaultInsertHandler.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2012, Thilo Planz. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License, Version 2.0 + * as published by the Apache Software Foundation (the "License"). + * + * 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. + * + * You should have received a copy of the License along with this program. + * If not, see . + */ + +package jmockmongo; + +import java.util.Iterator; + +import org.bson.BSONObject; + +class DefaultInsertHandler implements InsertHandler { + + private final MockMongo mongo; + + DefaultInsertHandler(MockMongo mongo) { + this.mongo = mongo; + } + + public void handleInsert(String database, String collection, + boolean continueOnError, Iterator docs) { + + MockDBCollection c = mongo.getOrCreateDB(database) + .getOrCreateCollection(collection); + while (docs.hasNext()) { + c.insert(docs.next()); + } + + } + +} diff --git a/src/main/java/jmockmongo/DefaultQueryHandler.java b/src/main/java/jmockmongo/DefaultQueryHandler.java new file mode 100644 index 0000000..db2c4d9 --- /dev/null +++ b/src/main/java/jmockmongo/DefaultQueryHandler.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2012, Thilo Planz. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License, Version 2.0 + * as published by the Apache Software Foundation (the "License"). + * + * 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. + * + * You should have received a copy of the License along with this program. + * If not, see . + */ + +package jmockmongo; + +import org.bson.BSONObject; + +class DefaultQueryHandler implements QueryHandler { + + private final MockMongo mongo;; + + public DefaultQueryHandler(MockMongo mongo) { + this.mongo = mongo; + } + + public BSONObject[] handleQuery(String database, String collection, + BSONObject command) { + + if ("system.indexes".equals(collection)) + return new BSONObject[0]; + + MockDB db = mongo.getDB(database); + if (db != null) { + Object id = command.get("_id"); + if (id == null || command.keySet().size() > 1) + throw new UnsupportedOperationException( + "only _id queries are implemented, not " + command); + MockDBCollection c = db.getCollection(collection); + if (c != null) { + BSONObject result = c.findOne(id); + if (result != null) + return new BSONObject[] { result }; + } + } + + return new BSONObject[0]; + } + +} diff --git a/src/main/java/jmockmongo/InsertHandler.java b/src/main/java/jmockmongo/InsertHandler.java new file mode 100644 index 0000000..370bb06 --- /dev/null +++ b/src/main/java/jmockmongo/InsertHandler.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2012, Thilo Planz. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License, Version 2.0 + * as published by the Apache Software Foundation (the "License"). + * + * 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. + * + * You should have received a copy of the License along with this program. + * If not, see . + */ + +package jmockmongo; + +import java.util.Iterator; + +import org.bson.BSONObject; + +public interface InsertHandler { + + public void handleInsert(String database, String collection, + boolean continueOnError, Iterator data); +} diff --git a/src/main/java/jmockmongo/MockDB.java b/src/main/java/jmockmongo/MockDB.java new file mode 100644 index 0000000..dd9aa62 --- /dev/null +++ b/src/main/java/jmockmongo/MockDB.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2012, Thilo Planz. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License, Version 2.0 + * as published by the Apache Software Foundation (the "License"). + * + * 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. + * + * You should have received a copy of the License along with this program. + * If not, see . + */ + +package jmockmongo; + +import java.util.concurrent.ConcurrentHashMap; + +public class MockDB { + + private final String name; + + private final ConcurrentHashMap collections = new ConcurrentHashMap(); + + MockDB(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public MockDBCollection getCollection(String collectionName) { + return collections.get(collectionName); + + } + + public MockDBCollection getOrCreateCollection(String collectionName) { + MockDBCollection collection = collections.get(collectionName); + if (collection != null) + return collection; + collections.putIfAbsent(collectionName, new MockDBCollection( + collectionName)); + return collections.get(collectionName); + } + +} diff --git a/src/main/java/jmockmongo/MockDBCollection.java b/src/main/java/jmockmongo/MockDBCollection.java new file mode 100644 index 0000000..adc72fd --- /dev/null +++ b/src/main/java/jmockmongo/MockDBCollection.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2012, Thilo Planz. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License, Version 2.0 + * as published by the Apache Software Foundation (the "License"). + * + * 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. + * + * You should have received a copy of the License along with this program. + * If not, see . + */ + +package jmockmongo; + +import java.math.BigInteger; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.bson.BSONObject; +import org.bson.types.ObjectId; + +public class MockDBCollection { + + private final String name; + + private final Map data = new ConcurrentHashMap(); + + MockDBCollection(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public BSONObject findOne(Object id) { + // byte[] don't hash... + if (id instanceof byte[]) { + id = new BigInteger((byte[]) id); + } + return data.get(id); + } + + public void insert(BSONObject b) { + if (!b.containsField("_id")) { + b.put("_id", new ObjectId()); + } + Object id = b.get("_id"); + // byte[] don't hash... + if (id instanceof byte[]) { + id = new BigInteger((byte[]) id); + } + data.put(id, b); + } +} diff --git a/src/main/java/jmockmongo/MockMongo.java b/src/main/java/jmockmongo/MockMongo.java new file mode 100644 index 0000000..99c749a --- /dev/null +++ b/src/main/java/jmockmongo/MockMongo.java @@ -0,0 +1,132 @@ +/** + * Copyright (c) 2012, Thilo Planz. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License, Version 2.0 + * as published by the Apache Software Foundation (the "License"). + * + * 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. + * + * You should have received a copy of the License along with this program. + * If not, see . + */ + +package jmockmongo; + +import java.net.InetSocketAddress; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; + +import jmockmongo.commands.Count; +import jmockmongo.commands.GetLastError; +import jmockmongo.commands.IsMaster; +import jmockmongo.commands.ListDatabases; +import jmockmongo.wire.LoggingHandler; +import jmockmongo.wire.MessageDecoder; +import jmockmongo.wire.ReplyHandler; + +import org.jboss.netty.bootstrap.ServerBootstrap; +import org.jboss.netty.channel.ChannelFactory; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.ChannelPipelineFactory; +import org.jboss.netty.channel.ChannelStateEvent; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.channel.SimpleChannelHandler; +import org.jboss.netty.channel.group.ChannelGroup; +import org.jboss.netty.channel.group.DefaultChannelGroup; +import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; + +import com.mongodb.DBAddress; + +public class MockMongo { + + private ChannelGroup channels; + + private ServerBootstrap bootstrap; + + private ConcurrentHashMap data; + + MockDB getDB(String database) { + return data.get(database); + } + + MockDB getOrCreateDB(String database) { + MockDB db = data.get(database); + if (db != null) + return db; + data.putIfAbsent(database, new MockDB(database)); + return data.get(database); + } + + MockDBCollection getCollection(String fullCollectionName) { + String[] fc = fullCollectionName.split("\\.", 2); + MockDB db = getDB(fc[0]); + if (db == null) + return null; + return db.getCollection(fc[1]); + } + + MockDBCollection getCollection(String database, String collectionName) { + MockDB db = getDB(database); + if (db == null) + return null; + return db.getCollection(collectionName); + } + + public void start() { + data = new ConcurrentHashMap(); + final ChannelFactory factory = new NioServerSocketChannelFactory( + Executors.newCachedThreadPool(), Executors + .newCachedThreadPool()); + + channels = new DefaultChannelGroup("jmockmongo"); + bootstrap = new ServerBootstrap(factory); + + final ReplyHandler handler = new ReplyHandler(); + handler.setCommandHandler("isMaster", new IsMaster()); + handler.setCommandHandler("listDatabases", new ListDatabases()); + handler.setCommandHandler("count", new Count()); + handler.setCommandHandler("getlasterror", new GetLastError()); + + handler.setQueryHandler(new DefaultQueryHandler(this)); + + handler.setInsertHandler(new DefaultInsertHandler(this)); + + bootstrap.setPipelineFactory(new ChannelPipelineFactory() { + public ChannelPipeline getPipeline() { + return Channels.pipeline(new SimpleChannelHandler() { + + @Override + public void channelOpen(ChannelHandlerContext ctx, + ChannelStateEvent e) throws Exception { + channels.add(e.getChannel()); + super.channelOpen(ctx, e); + } + + }, new MessageDecoder(), new LoggingHandler(), handler); + } + }); + + channels.add(bootstrap.bind(new InetSocketAddress(DBAddress + .defaultPort()))); + } + + public void stop() { + if (channels != null) { + channels.close().awaitUninterruptibly(); + } + channels = null; + if (bootstrap != null) { + bootstrap.releaseExternalResources(); + } + bootstrap = null; + + } + +} diff --git a/src/main/java/jmockmongo/QueryHandler.java b/src/main/java/jmockmongo/QueryHandler.java new file mode 100644 index 0000000..4693e7b --- /dev/null +++ b/src/main/java/jmockmongo/QueryHandler.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2012, Thilo Planz. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License, Version 2.0 + * as published by the Apache Software Foundation (the "License"). + * + * 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. + * + * You should have received a copy of the License along with this program. + * If not, see . + */ + +package jmockmongo; + +import org.bson.BSONObject; + +public interface QueryHandler { + + public BSONObject[] handleQuery(String database, String collection, + BSONObject command); + +} diff --git a/src/main/java/jmockmongo/commands/Count.java b/src/main/java/jmockmongo/commands/Count.java new file mode 100644 index 0000000..628ac4b --- /dev/null +++ b/src/main/java/jmockmongo/commands/Count.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2012, Thilo Planz. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License, Version 2.0 + * as published by the Apache Software Foundation (the "License"). + * + * 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. + * + * You should have received a copy of the License along with this program. + * If not, see . + */ + +package jmockmongo.commands; + +import jmockmongo.CommandHandler; + +import org.bson.BSONObject; +import org.bson.BasicBSONObject; + +public class Count implements CommandHandler { + + public BSONObject handleCommand(String database, BSONObject command) { + return new BasicBSONObject("ok", 1).append("n", 0); + } + +} diff --git a/src/main/java/jmockmongo/commands/GetLastError.java b/src/main/java/jmockmongo/commands/GetLastError.java new file mode 100644 index 0000000..f102a31 --- /dev/null +++ b/src/main/java/jmockmongo/commands/GetLastError.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2012, Thilo Planz. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License, Version 2.0 + * as published by the Apache Software Foundation (the "License"). + * + * 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. + * + * You should have received a copy of the License along with this program. + * If not, see . + */ + +package jmockmongo.commands; + +import jmockmongo.CommandHandler; + +import org.bson.BSONObject; +import org.bson.BasicBSONObject; + +public class GetLastError implements CommandHandler { + + public BSONObject handleCommand(String database, BSONObject command) { + return new BasicBSONObject("ok", 1); + } + +} diff --git a/src/main/java/jmockmongo/commands/IsMaster.java b/src/main/java/jmockmongo/commands/IsMaster.java new file mode 100644 index 0000000..854719f --- /dev/null +++ b/src/main/java/jmockmongo/commands/IsMaster.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2012, Thilo Planz. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License, Version 2.0 + * as published by the Apache Software Foundation (the "License"). + * + * 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. + * + * You should have received a copy of the License along with this program. + * If not, see . + */ + +package jmockmongo.commands; + +import jmockmongo.CommandHandler; + +import org.bson.BSONObject; +import org.bson.BasicBSONObject; + +public class IsMaster implements CommandHandler { + + public BSONObject handleCommand(String database, BSONObject command) { + return new BasicBSONObject("ismaster", 1); + } + +} diff --git a/src/main/java/jmockmongo/commands/ListDatabases.java b/src/main/java/jmockmongo/commands/ListDatabases.java new file mode 100644 index 0000000..63681ea --- /dev/null +++ b/src/main/java/jmockmongo/commands/ListDatabases.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2012, Thilo Planz. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License, Version 2.0 + * as published by the Apache Software Foundation (the "License"). + * + * 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. + * + * You should have received a copy of the License along with this program. + * If not, see . + */ + +package jmockmongo.commands; + +import jmockmongo.CommandHandler; + +import org.bson.BSONObject; +import org.bson.BasicBSONObject; + +public class ListDatabases implements CommandHandler { + + public BSONObject handleCommand(String database, BSONObject command) { + return new BasicBSONObject("ok", 1).append("databases", new Object[0]); + } + +} diff --git a/src/main/java/jmockmongo/wire/InsertMessage.java b/src/main/java/jmockmongo/wire/InsertMessage.java new file mode 100644 index 0000000..60ed15e --- /dev/null +++ b/src/main/java/jmockmongo/wire/InsertMessage.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2012, Thilo Planz. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License, Version 2.0 + * as published by the Apache Software Foundation (the "License"). + * + * 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. + * + * You should have received a copy of the License along with this program. + * If not, see . + */ + +package jmockmongo.wire; + +import static jmockmongo.wire.MessageDecoder.readCString; +import static jmockmongo.wire.MessageDecoder.readDocument; +import static jmockmongo.wire.MessageDecoder.readInt32; + +import java.util.Iterator; + +import org.bson.BSONObject; +import org.jboss.netty.buffer.ChannelBuffer; + +class InsertMessage extends Message { + + static final int OP_CODE_INSERT = 2002; + + // struct { + // MsgHeader header; // standard message header + // int32 flags; // bit vector - see below + // cstring fullCollectionName; // "dbname.collectionname" + // document* documents; // one or more documents to insert into the + // collection + // } + + private final int flags; + + private final String fullCollectionName; + + String getFullCollectionName() { + return fullCollectionName; + } + + InsertMessage(ChannelBuffer data) { + super(data, OP_CODE_INSERT); + byte[] fourBytes = new byte[4]; + flags = readInt32(data, fourBytes); + fullCollectionName = readCString(data); + } + + Iterator documents() { + final ChannelBuffer copy = data.slice(); + final byte[] fourBytes = new byte[4]; + return new Iterator() { + + public boolean hasNext() { + return copy.readable(); + } + + public BSONObject next() { + return readDocument(copy, fourBytes); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public String toString() { + return super.toString() + " collection: " + fullCollectionName; + } +} diff --git a/src/main/java/jmockmongo/wire/LoggingHandler.java b/src/main/java/jmockmongo/wire/LoggingHandler.java new file mode 100644 index 0000000..d391620 --- /dev/null +++ b/src/main/java/jmockmongo/wire/LoggingHandler.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2012, Thilo Planz. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License, Version 2.0 + * as published by the Apache Software Foundation (the "License"). + * + * 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. + * + * You should have received a copy of the License along with this program. + * If not, see . + */ + +package jmockmongo.wire; + +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelUpstreamHandler; + +public class LoggingHandler extends SimpleChannelUpstreamHandler { + + @Override + public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) + throws Exception { + System.out.println(e.getMessage()); + super.messageReceived(ctx, e); + } + +} diff --git a/src/main/java/jmockmongo/wire/Message.java b/src/main/java/jmockmongo/wire/Message.java new file mode 100644 index 0000000..c134057 --- /dev/null +++ b/src/main/java/jmockmongo/wire/Message.java @@ -0,0 +1,159 @@ +/** + * Copyright (c) 2012, Thilo Planz. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License, Version 2.0 + * as published by the Apache Software Foundation (the "License"). + * + * 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. + * + * You should have received a copy of the License along with this program. + * If not, see . + */ + +package jmockmongo.wire; + +import static jmockmongo.wire.MessageDecoder.readInt32; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; + +abstract class Message { + + // struct MsgHeader { + // int32 messageLength; // total message size, including this + // int32 requestID; // identifier for this message + // int32 responseTo; // requestID from the original request + // // (used in reponses from db) + // int32 opCode; // request type - see table below + // } + + private final int messageLength; + + private final int requestId; + + private final int responseTo; + + protected final int opCode; + + protected final ChannelBuffer data; + + Message(ChannelBuffer data, int expectedOpCode) { + byte[] fourBytes = new byte[4]; + + int check = data.readableBytes(); + this.messageLength = readInt32(data, fourBytes); + if (check != messageLength) { + throw new IllegalArgumentException(String.format( + "message buffer has unexpected size (%d instead of %d)", + check, messageLength)); + } + this.requestId = readInt32(data, fourBytes); + this.responseTo = readInt32(data, fourBytes); + this.opCode = readInt32(data, fourBytes); + this.data = data; + if (opCode != expectedOpCode) { + throw new IllegalArgumentException(String.format( + "message buffer has unexpected opcode (%d instead of %d)", + opCode, expectedOpCode)); + } + } + + static void writeInt(ChannelBuffer buffer, int x) { + buffer.writeByte(x >> 0); + buffer.writeByte(x >> 8); + buffer.writeByte(x >> 16); + buffer.writeByte(x >> 24); + } + + static void writeLong(ChannelBuffer buffer, long x) { + buffer.writeByte((byte) (0xFFL & (x >> 0))); + buffer.writeByte((byte) (0xFFL & (x >> 8))); + buffer.writeByte((byte) (0xFFL & (x >> 16))); + buffer.writeByte((byte) (0xFFL & (x >> 24))); + buffer.writeByte((byte) (0xFFL & (x >> 32))); + buffer.writeByte((byte) (0xFFL & (x >> 40))); + buffer.writeByte((byte) (0xFFL & (x >> 48))); + buffer.writeByte((byte) (0xFFL & (x >> 56))); + } + + static void outputMessageHeader(ChannelBuffer buffer, int messageLength, + int requestId, int responseTo, int opCode) { + writeInt(buffer, messageLength); + writeInt(buffer, requestId); + writeInt(buffer, responseTo); + writeInt(buffer, opCode); + } + + static Message readMessage(ChannelBuffer buffer, int messageLength) { + byte[] fourBytes = new byte[4]; + // messageLength + readInt32(buffer, fourBytes); + // requestId + readInt32(buffer, fourBytes); + // responseTo + readInt32(buffer, fourBytes); + int opCode = readInt32(buffer, fourBytes); + buffer.resetReaderIndex(); + switch (opCode) { + case ReplyMessage.OP_CODE_REPLY: + return new ReplyMessage(buffer.readBytes(messageLength)); + case QueryMessage.OP_CODE_QUERY: + return new QueryMessage(buffer.readBytes(messageLength)); + case InsertMessage.OP_CODE_INSERT: + return new InsertMessage(buffer.readBytes(messageLength)); + default: + throw new IllegalArgumentException(String.format( + "message buffer has unsupported opcode (%d)", opCode)); + + } + } + + String getOpCodeName() { + switch (opCode) { + case 1: + return "OP_REPLY"; + case 1000: + return "OP_MSG"; + case 2001: + return "OP_UPDATE"; + case 2002: + return "OP_INSERT"; + case 2003: + return "RESERVED"; + case 2004: + return "OP_QUERY"; + case 2005: + return "OP_GET_MORE"; + case 2006: + return "OP_DELETE"; + case 2007: + return "OP_KILL_CURSORS"; + } + return String.format("?(%d)", opCode); + } + + ChannelBuffer getBytes() { + return ChannelBuffers.unmodifiableBuffer(data.slice(0, messageLength)); + } + + int getRequestId() { + return requestId; + } + + @Override + public String toString() { + if (responseTo != 0) { + return "[" + getOpCodeName() + "] Re:#" + responseTo + " (" + + messageLength + " bytes)"; + } + return "[" + getOpCodeName() + "] #" + requestId + " (" + messageLength + + " bytes)"; + } + +} diff --git a/src/main/java/jmockmongo/wire/MessageDecoder.java b/src/main/java/jmockmongo/wire/MessageDecoder.java new file mode 100644 index 0000000..d35273e --- /dev/null +++ b/src/main/java/jmockmongo/wire/MessageDecoder.java @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2012, Thilo Planz. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License, Version 2.0 + * as published by the Apache Software Foundation (the "License"). + * + * 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. + * + * You should have received a copy of the License along with this program. + * If not, see . + */ + +package jmockmongo.wire; + +import org.bson.BSON; +import org.bson.BSONException; +import org.bson.BSONObject; +import org.bson.io.Bits; +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.handler.codec.frame.FrameDecoder; + +public class MessageDecoder extends FrameDecoder { + + static final int readInt32(ChannelBuffer buffer, byte[] fourBytes) { + buffer.readBytes(fourBytes); + return Bits.readInt(fourBytes); + } + + static final long readInt64(ChannelBuffer buffer, byte[] eightBytes) { + buffer.readBytes(eightBytes); + return Bits.readLong(eightBytes); + } + + static final BSONObject readDocument(ChannelBuffer buffer, byte[] fourBytes) { + int length = readInt32(buffer, fourBytes); + byte[] bson = new byte[length]; + buffer.readBytes(bson, 4, length - 4); + System.arraycopy(fourBytes, 0, bson, 0, 4); + return BSON.decode(bson); + + } + + static final String readCString(ChannelBuffer buffer) { + // find the terminating zero-byte + int length = buffer.bytesBefore((byte) 0); + byte[] bytes = new byte[length]; + buffer.readBytes(bytes); + // also skip the terminator + buffer.readByte(); + try { + return new String(bytes, "UTF-8"); + } catch (java.io.UnsupportedEncodingException uee) { + throw new BSONException("impossible", uee); + } + } + + static final String readUTF8String(ChannelBuffer buffer, byte[] fourBytes) { + int size = readInt32(buffer, fourBytes); + // this is just protection in case it's corrupted, to avoid huge strings + if (size <= 0 || size > (32 * 1024 * 1024)) + throw new BSONException("bad string size: " + size); + + byte[] b = new byte[size]; + buffer.readBytes(b); + + try { + return new String(b, 0, size - 1, "UTF-8"); + } catch (java.io.UnsupportedEncodingException uee) { + throw new BSONException("impossible", uee); + } + } + + @Override + protected Object decode(ChannelHandlerContext ctx, Channel channel, + ChannelBuffer buffer) throws Exception { + + // struct MsgHeader { + // int32 messageLength; // total message size, including this + // int32 requestID; // identifier for this message + // int32 responseTo; // requestID from the original request + // // (used in reponses from db) + // int32 opCode; // request type - see table below + // } + + if (buffer.readableBytes() < 4) + return null; + byte[] fourBytes = new byte[4]; + buffer.markReaderIndex(); + int messageLength = readInt32(buffer, fourBytes); + buffer.resetReaderIndex(); + + if (buffer.readableBytes() < messageLength) { + return null; + } + + return Message.readMessage(buffer, messageLength); + + } +} diff --git a/src/main/java/jmockmongo/wire/QueryMessage.java b/src/main/java/jmockmongo/wire/QueryMessage.java new file mode 100644 index 0000000..90167a8 --- /dev/null +++ b/src/main/java/jmockmongo/wire/QueryMessage.java @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2012, Thilo Planz. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License, Version 2.0 + * as published by the Apache Software Foundation (the "License"). + * + * 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. + * + * You should have received a copy of the License along with this program. + * If not, see . + */ + +package jmockmongo.wire; + +import static jmockmongo.wire.MessageDecoder.readCString; +import static jmockmongo.wire.MessageDecoder.readDocument; +import static jmockmongo.wire.MessageDecoder.readInt32; + +import org.bson.BSONObject; +import org.jboss.netty.buffer.ChannelBuffer; + +class QueryMessage extends Message { + + static final int OP_CODE_QUERY = 2004; + + // struct OP_QUERY { + // MsgHeader header; // standard message header + // int32 flags; // bit vector of query options. See below for details. + // cstring fullCollectionName; // "dbname.collectionname" + // int32 numberToSkip; // number of documents to skip + // int32 numberToReturn; // number of documents to return + // // in the first OP_REPLY batch + // document query; // query object. See below for details. + // [ document returnFieldSelector; ] // Optional. Selector indicating the + // fields + // // to return. See below for details. + // } + + private final int flags; + + private final String fullCollectionName; + + private final int numberToSkip; + + private final int numberToReturn; + + private BSONObject query; + + private BSONObject returnFieldSelector; + + QueryMessage(ChannelBuffer data) { + super(data, OP_CODE_QUERY); + byte[] fourBytes = new byte[4]; + flags = readInt32(data, fourBytes); + fullCollectionName = readCString(data); + numberToSkip = readInt32(data, fourBytes); + numberToReturn = readInt32(data, fourBytes); + } + + BSONObject getQuery() { + if (query == null) { + byte[] fourBytes = new byte[4]; + query = readDocument(data, fourBytes); + if (data.readable()) + returnFieldSelector = readDocument(data, fourBytes); + } + return query; + } + + BSONObject getReturnFieldSelector() { + if (query == null) + getQuery(); + return returnFieldSelector; + } + + String getFullCollectionName() { + return fullCollectionName; + } + + @Override + public String toString() { + return super.toString() + + " collection: " + + fullCollectionName + + " query: " + + getQuery() + + (numberToSkip > 0 ? " skip: " + numberToSkip : "") + + (numberToReturn > 0 ? " return: " + numberToReturn : "") + + (returnFieldSelector != null ? " returnFieldSelector: " + + returnFieldSelector : ""); + } + +} diff --git a/src/main/java/jmockmongo/wire/ReplyHandler.java b/src/main/java/jmockmongo/wire/ReplyHandler.java new file mode 100644 index 0000000..8c0869c --- /dev/null +++ b/src/main/java/jmockmongo/wire/ReplyHandler.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2012, Thilo Planz. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License, Version 2.0 + * as published by the Apache Software Foundation (the "License"). + * + * 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. + * + * You should have received a copy of the License along with this program. + * If not, see . + */ + +package jmockmongo.wire; + +import java.util.HashMap; +import java.util.Map; + +import jmockmongo.CommandHandler; +import jmockmongo.InsertHandler; +import jmockmongo.QueryHandler; + +import org.bson.BSONObject; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelUpstreamHandler; + +public class ReplyHandler extends SimpleChannelUpstreamHandler { + + private final Map commands = new HashMap(); + + private QueryHandler queryHandler; + + private InsertHandler insertHandler; + + public void setCommandHandler(String command, CommandHandler handler) { + commands.put(command, handler); + } + + public void setQueryHandler(QueryHandler handler) { + queryHandler = handler; + } + + public void setInsertHandler(InsertHandler handler) { + insertHandler = handler; + } + + @Override + public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) + throws Exception { + + Message request = (Message) e.getMessage(); + if (request instanceof QueryMessage) { + QueryMessage query = (QueryMessage) request; + String fc = query.getFullCollectionName(); + String[] db = fc.split("\\.", 2); + + if ("$cmd".equals(db[1])) { + BSONObject command = query.getQuery(); + String c = command.keySet().iterator().next(); + CommandHandler handler = commands.get(c); + if (handler != null) { + BSONObject result = handler.handleCommand(db[0], command); + e.getChannel().write( + ReplyMessage.reply((Message) e.getMessage(), 0, 0, + 0, result).getBytes()); + return; + } + + throw new UnsupportedOperationException("command " + c + " " + + request.toString()); + + } + if (queryHandler != null) { + BSONObject[] result = queryHandler.handleQuery(db[0], db[1], + query.getQuery()); + e.getChannel().write( + ReplyMessage.reply((Message) e.getMessage(), 0, 0, 0, + result).getBytes()); + return; + } + + } + if (request instanceof InsertMessage) { + if (insertHandler != null) { + InsertMessage insert = (InsertMessage) request; + String fc = insert.getFullCollectionName(); + String[] db = fc.split("\\.", 2); + insertHandler.handleInsert(db[0], db[1], false, insert + .documents()); + return; + } + } + + throw new UnsupportedOperationException(request.toString()); + + } +} diff --git a/src/main/java/jmockmongo/wire/ReplyMessage.java b/src/main/java/jmockmongo/wire/ReplyMessage.java new file mode 100644 index 0000000..95fdeeb --- /dev/null +++ b/src/main/java/jmockmongo/wire/ReplyMessage.java @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2012, Thilo Planz. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License, Version 2.0 + * as published by the Apache Software Foundation (the "License"). + * + * 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. + * + * You should have received a copy of the License along with this program. + * If not, see . + */ + +package jmockmongo.wire; + +import static jmockmongo.wire.MessageDecoder.readInt32; + +import org.bson.BSON; +import org.bson.BSONObject; +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; + +class ReplyMessage extends Message { + + static final int OP_CODE_REPLY = 1; + + // struct { + // MsgHeader header; // standard message header + // int32 responseFlags; // bit vector - see details below + // int64 cursorID; // cursor id if client needs to do get more's + // int32 startingFrom; // where in the cursor this reply is starting + // int32 numberReturned; // number of documents in the reply + // document* documents; // documents + // } + // + + private final int responseFlags; + + private final long cursorId; + + private final int startingFrom; + + private final int numberReturned; + + ReplyMessage(ChannelBuffer data) { + super(data, OP_CODE_REPLY); + byte[] fourBytes = new byte[4]; + byte[] eightBytes = new byte[8]; + responseFlags = readInt32(data, fourBytes); + cursorId = MessageDecoder.readInt64(data, eightBytes); + startingFrom = readInt32(data, fourBytes); + numberReturned = readInt32(data, fourBytes); + } + + @Override + public String toString() { + if (cursorId != 0) { + return super.toString() + " [cursor #" + cursorId + "@" + + startingFrom + "(" + numberReturned + ")]"; + } + return super.toString() + " with " + numberReturned + " docs"; + } + + static ReplyMessage reply(Message request, int responseFlags, + long cursorId, int startingFrom, BSONObject... docs) { + + ChannelBuffer buffer = ChannelBuffers.dynamicBuffer(); + writeInt(buffer, responseFlags); + writeLong(buffer, cursorId); + writeInt(buffer, startingFrom); + writeInt(buffer, docs.length); + for (BSONObject doc : docs) + buffer.writeBytes(BSON.encode(doc)); + ChannelBuffer header = ChannelBuffers.buffer(16); + outputMessageHeader(header, buffer.readableBytes() + 16, 1, request + .getRequestId(), OP_CODE_REPLY); + + return new ReplyMessage(ChannelBuffers.wrappedBuffer(header, buffer)); + } +} diff --git a/src/test/java/jmockmongo/DefaultInsertHandlerTest.java b/src/test/java/jmockmongo/DefaultInsertHandlerTest.java new file mode 100644 index 0000000..ab375d3 --- /dev/null +++ b/src/test/java/jmockmongo/DefaultInsertHandlerTest.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2012, Thilo Planz. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License, Version 2.0 + * as published by the Apache Software Foundation (the "License"). + * + * 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. + * + * You should have received a copy of the License along with this program. + * If not, see . + */ + +package jmockmongo; + +import java.net.UnknownHostException; + +import com.mongodb.BasicDBObject; +import com.mongodb.Mongo; +import com.mongodb.MongoException; +import com.mongodb.WriteConcern; + +public class DefaultInsertHandlerTest extends MockMongoSetup { + + public void testSimpleInsert() throws UnknownHostException, MongoException, + InterruptedException { + + Mongo m = new Mongo(); + m.getDB("x").getCollection("x").insert(WriteConcern.SAFE, + new BasicDBObject("_id", "x").append("field", "test")); + + assertMockMongoFieldEquals("test", "x.x", "x", "field"); + assertEquals("test", m.getDB("x").getCollection("x").findOne("x").get( + "field")); + + } +} diff --git a/src/test/java/jmockmongo/MockMongoSetup.java b/src/test/java/jmockmongo/MockMongoSetup.java new file mode 100644 index 0000000..aec4fad --- /dev/null +++ b/src/test/java/jmockmongo/MockMongoSetup.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2012, Thilo Planz. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License, Version 2.0 + * as published by the Apache Software Foundation (the "License"). + * + * 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. + * + * You should have received a copy of the License along with this program. + * If not, see . + */ + +package jmockmongo; + +import junit.framework.TestCase; + +import org.bson.BSONObject; + +public abstract class MockMongoSetup extends TestCase { + + private MockMongo mongo; + + @Override + protected void setUp() throws Exception { + mongo = new MockMongo(); + mongo.start(); + } + + @Override + protected void tearDown() throws Exception { + mongo.stop(); + } + + protected BSONObject assertMockMongoContainsDocument( + String fullCollectionName, Object _id) { + MockDBCollection c = mongo.getCollection(fullCollectionName); + if (c == null) { + fail("no collection " + fullCollectionName); + } + BSONObject b = c.findOne(_id); + if (b == null) { + fail("no document " + _id + " in " + fullCollectionName); + } + return b; + } + + protected void assertMockMongoFieldEquals(Object expected, + String fullCollectionName, Object _id, String field) { + BSONObject doc = assertMockMongoContainsDocument(fullCollectionName, + _id); + assertEquals(expected, BSONUtils.get(doc, field)); + } + +} diff --git a/src/test/java/jmockmongo/MockMongoTest.java b/src/test/java/jmockmongo/MockMongoTest.java new file mode 100644 index 0000000..b344692 --- /dev/null +++ b/src/test/java/jmockmongo/MockMongoTest.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2012, Thilo Planz. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License, Version 2.0 + * as published by the Apache Software Foundation (the "License"). + * + * 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. + * + * You should have received a copy of the License along with this program. + * If not, see . + */ + +package jmockmongo; + +import java.net.UnknownHostException; + +import junit.framework.TestCase; + +import com.mongodb.Mongo; +import com.mongodb.MongoException; + +public class MockMongoTest extends TestCase { + + public void testStartStop() throws UnknownHostException, MongoException, + InterruptedException { + + MockMongo mongo = new MockMongo(); + mongo.start(); + new Mongo().getDatabaseNames(); + mongo.stop(); + try { + new Mongo().getDatabaseNames(); + fail("should have stopped"); + } catch (MongoException e) { + } + } + +}