/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.mahout.cf.taste.impl.model.mongodb; import java.text.DateFormat; import java.text.ParseException; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.locks.ReentrantLock; import java.net.UnknownHostException; import java.text.SimpleDateFormat; import java.util.regex.Pattern; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import org.apache.mahout.cf.taste.common.Refreshable; import org.apache.mahout.cf.taste.common.TasteException; import org.apache.mahout.cf.taste.impl.common.FastByIDMap; import org.apache.mahout.cf.taste.impl.common.FastIDSet; import org.apache.mahout.cf.taste.impl.common.LongPrimitiveIterator; import org.apache.mahout.cf.taste.impl.model.GenericDataModel; import org.apache.mahout.cf.taste.impl.model.GenericPreference; import org.apache.mahout.cf.taste.impl.model.GenericUserPreferenceArray; import org.apache.mahout.cf.taste.model.DataModel; import org.apache.mahout.cf.taste.model.Preference; import org.apache.mahout.cf.taste.model.PreferenceArray; import org.apache.mahout.cf.taste.common.NoSuchUserException; import org.apache.mahout.cf.taste.common.NoSuchItemException; import org.bson.types.ObjectId; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mongodb.BasicDBObject; import com.mongodb.DBObject; import com.mongodb.Mongo; import com.mongodb.DB; import com.mongodb.DBCollection; import com.mongodb.DBCursor; import com.mongodb.MongoException; /** * <p>A {@link DataModel} backed by a MongoDB database. This class expects a * collection in the database which contains a user ID ({@code long} or * {@link ObjectId}), item ID ({@code long} or * {@link ObjectId}), preference value (optional) and timestamps * ("created_at", "deleted_at").</p> * * <p>An example of a document in MongoDB:</p> * * <p>{@code { "_id" : ObjectId("4d7627bf6c7d47ade9fc7780"), * "user_id" : ObjectId("4c2209fef3924d31102bd84b"), * "item_id" : ObjectId(4c2209fef3924d31202bd853), * "preference" : 0.5, * "created_at" : "Tue Mar 23 2010 20:48:43 GMT-0400 (EDT)" } * }</p> * * <p>Preference value is optional to accommodate applications that have no notion * of a preference value (that is, the user simply expresses a preference for * an item, but no degree of preference).</p> * * <p>The preference value is assumed to be parseable as a {@code double}.</p> * * <p>The user IDs and item IDs are assumed to be parseable as {@code long}s * or {@link ObjectId}s. In case of {@link ObjectId}s, the * model creates a {@code Map<ObjectId>}, {@code long}> * (collection "mongo_data_model_map") inside the MongoDB database. This * conversion is needed since Mahout uses the long datatype to feed the * recommender, and MongoDB uses 12 bytes to create its identifiers.</p> * * <p>The timestamps ("created_at", "deleted_at"), if present, are assumed to be * parseable as a {@code long} or {@link Date}. To express * timestamps as {@link Date}s, a {@link DateFormat} * must be provided in the class constructor. The default Date format is * {@code "EE MMM dd yyyy HH:mm:ss 'GMT'Z (zzz)"}. If this parameter * is set to null, timestamps are assumed to be parseable as {@code long}s. * </p> * * <p>It is also acceptable for the documents to contain additional fields. * Those fields will be ignored.</p> * * <p>This class will reload data from the MondoDB database when * {@link #refresh(Collection)} is called. MongoDBDataModel keeps the * timestamp of the last update. This variable and the fields "created_at" * and "deleted_at" help the model to determine if the triple * (user, item, preference) must be added or deleted.</p> */ public final class MongoDBDataModel implements DataModel { private static final Logger log = LoggerFactory.getLogger(MongoDBDataModel.class); /** Default MongoDB host. Default: localhost */ private static final String DEFAULT_MONGO_HOST = "localhost"; /** Default MongoDB port. Default: 27017 */ private static final int DEFAULT_MONGO_PORT = 27017; /** Default MongoDB database. Default: recommender */ private static final String DEFAULT_MONGO_DB = "recommender"; /** * Default MongoDB authentication flag. * Default: false (authentication is not required) */ private static final boolean DEFAULT_MONGO_AUTH = false; /** Default MongoDB user. Default: recommender */ private static final String DEFAULT_MONGO_USERNAME = "recommender"; /** Default MongoDB password. Default: recommender */ private static final String DEFAULT_MONGO_PASSWORD = "recommender"; /** Default MongoDB table/collection. Default: items */ private static final String DEFAULT_MONGO_COLLECTION = "items"; /** * Default MongoDB update flag. When this flag is activated, the * DataModel updates both model and database. Default: true */ private static final boolean DEFAULT_MONGO_MANAGE = true; /** Default MongoDB user ID field. Default: user_id */ private static final String DEFAULT_MONGO_USER_ID = "user_id"; /** Default MongoDB item ID field. Default: item_id */ private static final String DEFAULT_MONGO_ITEM_ID = "item_id"; /** Default MongoDB preference value field. Default: preference */ private static final String DEFAULT_MONGO_PREFERENCE = "preference"; /** Default MongoDB final remove flag. Default: false */ private static final boolean DEFAULT_MONGO_FINAL_REMOVE = false; /** * Default MongoDB date format. * Default: "EE MMM dd yyyy HH:mm:ss 'GMT'Z (zzz)" */ private static final DateFormat DEFAULT_DATE_FORMAT = new SimpleDateFormat("EE MMM dd yyyy HH:mm:ss 'GMT'Z (zzz)", Locale.ENGLISH); private static final String MONGO_MAP_COLLECTION = "mongo_data_model_map"; private static final Pattern ID_PATTERN = Pattern.compile("[a-f0-9]{24}"); /** MongoDB host */ private String mongoHost = DEFAULT_MONGO_HOST; /** MongoDB port */ private int mongoPort = DEFAULT_MONGO_PORT; /** MongoDB database */ private String mongoDB = DEFAULT_MONGO_DB; /** * MongoDB authentication flag. If this flag is set to false, * authentication is not required. */ private boolean mongoAuth = DEFAULT_MONGO_AUTH; /** MongoDB user */ private String mongoUsername = DEFAULT_MONGO_USERNAME; /** MongoDB pass */ private String mongoPassword = DEFAULT_MONGO_PASSWORD; /** MongoDB table/collection */ private String mongoCollection = DEFAULT_MONGO_COLLECTION; /** * MongoDB update flag. When this flag is activated, the * DataModel updates both model and database */ private boolean mongoManage = DEFAULT_MONGO_MANAGE; /** MongoDB user ID field */ private String mongoUserID = DEFAULT_MONGO_USER_ID; /** MongoDB item ID field */ private String mongoItemID = DEFAULT_MONGO_ITEM_ID; /** MongoDB preference value field */ private String mongoPreference = DEFAULT_MONGO_PREFERENCE; /** MongoDB final remove flag. Default: false */ private boolean mongoFinalRemove = DEFAULT_MONGO_FINAL_REMOVE; /** MongoDB date format */ private DateFormat dateFormat = DEFAULT_DATE_FORMAT; private DBCollection collection; private DBCollection collectionMap; private Date mongoTimestamp; private final ReentrantLock reloadLock; private DataModel delegate; private boolean userIsObject; private boolean itemIsObject; private boolean preferenceIsString; private long idCounter; /** * Creates a new MongoDBDataModel */ public MongoDBDataModel() throws UnknownHostException, MongoException { this.reloadLock = new ReentrantLock(); buildModel(); } /** * Creates a new MongoDBDataModel with MongoDB basic configuration * (without authentication) * * @param host MongoDB host. * @param port MongoDB port. Default: 27017 * @param database MongoDB database * @param collection MongoDB collection/table * @param manage If true, the model adds and removes users and items * from MongoDB database when the model is refreshed. * @param finalRemove If true, the model removes the user/item completely * from the MongoDB database. If false, the model adds the "deleted_at" * field with the current date to the "deleted" user/item. * @param format MongoDB date format. If null, the model uses timestamps. * @throws UnknownHostException if the database host cannot be resolved */ public MongoDBDataModel(String host, int port, String database, String collection, boolean manage, boolean finalRemove, DateFormat format) throws UnknownHostException, MongoException { mongoHost = host; mongoPort = port; mongoDB = database; mongoCollection = collection; mongoManage = manage; mongoFinalRemove = finalRemove; dateFormat = format; this.reloadLock = new ReentrantLock(); buildModel(); } /** * Creates a new MongoDBDataModel with MongoDB advanced configuration * (without authentication) * * @param userIDField Mongo user ID field * @param itemIDField Mongo item ID field * @param preferenceField Mongo preference value field * @throws UnknownHostException if the database host cannot be resolved * @see #MongoDBDataModel(String, int, String, String, boolean, boolean, DateFormat) */ public MongoDBDataModel(String host, int port, String database, String collection, boolean manage, boolean finalRemove, DateFormat format, String userIDField, String itemIDField, String preferenceField) throws UnknownHostException, MongoException { mongoHost = host; mongoPort = port; mongoDB = database; mongoCollection = collection; mongoManage = manage; mongoFinalRemove = finalRemove; dateFormat = format; mongoUserID = userIDField; mongoItemID = itemIDField; mongoPreference = preferenceField; this.reloadLock = new ReentrantLock(); buildModel(); } /** * Creates a new MongoDBDataModel with MongoDB basic configuration * (with authentication) * * @param user Mongo username (authentication) * @param password Mongo password (authentication) * @throws UnknownHostException if the database host cannot be resolved * @see #MongoDBDataModel(String, int, String, String, boolean, boolean, DateFormat) */ public MongoDBDataModel(String host, int port, String database, String collection, boolean manage, boolean finalRemove, DateFormat format, String user, String password) throws UnknownHostException, MongoException { mongoHost = host; mongoPort = port; mongoDB = database; mongoCollection = collection; mongoManage = manage; mongoFinalRemove = finalRemove; dateFormat = format; mongoAuth = true; mongoUsername = user; mongoPassword = password; this.reloadLock = new ReentrantLock(); buildModel(); } /** * Creates a new MongoDBDataModel with MongoDB advanced configuration * (with authentication) * * @throws UnknownHostException if the database host cannot be resolved * @see #MongoDBDataModel(String, int, String, String, boolean, boolean, DateFormat, String, String, String) * @see #MongoDBDataModel(String, int, String, String, boolean, boolean, DateFormat, String, String) */ public MongoDBDataModel(String host, int port, String database, String collection, boolean manage, boolean finalRemove, DateFormat format, String user, String password, String userIDField, String itemIDField, String preferenceField) throws UnknownHostException, MongoException { mongoHost = host; mongoPort = port; mongoDB = database; mongoCollection = collection; mongoManage = manage; mongoFinalRemove = finalRemove; dateFormat = format; mongoAuth = true; mongoUsername = user; mongoPassword = password; mongoUserID = userIDField; mongoItemID = itemIDField; mongoPreference = preferenceField; this.reloadLock = new ReentrantLock(); buildModel(); } /** * <p> * Adds/removes (user, item) pairs to/from the model. * </p> * * @param userID MongoDB user identifier * @param items List of pairs (item, preference) which want to be added or * deleted * @param add If true, this flag indicates that the pairs (user, item) * must be added to the model. If false, it indicates deletion. * @see #refresh(Collection) */ public void refreshData(String userID, Iterable<List<String>> items, boolean add) throws NoSuchUserException, NoSuchItemException { checkData(userID, items, add); long id = Long.parseLong(fromIdToLong(userID, true)); for (List<String> item : items) { item.set(0, fromIdToLong(item.get(0), false)); } if (reloadLock.tryLock()) { try { if (add) { delegate = addUserItem(id, items); } else { delegate = removeUserItem(id, items); } } finally { reloadLock.unlock(); } } } /** * <p> * Triggers "refresh" -- whatever that means -- of the implementation. * The general contract is that any should always leave itself in a * consistent, operational state, and that the refresh atomically updates * internal state from old to new. * </p> * * @param alreadyRefreshed s that are known to have already been refreshed as * a result of an initial call to a method on some object. This ensures * that objects in a refresh dependency graph aren't refreshed twice * needlessly. * @see #refreshData(String, Iterable, boolean) */ @Override public void refresh(Collection<Refreshable> alreadyRefreshed) { Date ts = new Date(0); BasicDBObject query = new BasicDBObject(); query.put("deleted_at", new BasicDBObject("$gt", mongoTimestamp)); DBCursor cursor = collection.find(query); while (cursor.hasNext()) { Map<String,Object> user = (Map<String,Object>) cursor.next().toMap(); String userID = getID(user.get(mongoUserID), true); Collection<List<String>> items = Lists.newArrayList(); List<String> item = Lists.newArrayList(); item.add(getID(user.get(mongoItemID), false)); item.add(Float.toString(getPreference(user.get(mongoPreference)))); items.add(item); try { refreshData(userID, items, false); } catch (NoSuchUserException e) { log.warn("No such user ID: {}", userID); } catch (NoSuchItemException e) { log.warn("No such items: {}", items); } if (ts.compareTo(getDate(user.get("created_at"))) < 0) { ts = getDate(user.get("created_at")); } } query = new BasicDBObject(); query.put("created_at", new BasicDBObject("$gt", mongoTimestamp)); cursor = collection.find(query); while (cursor.hasNext()) { Map<String,Object> user = (Map<String,Object>) cursor.next().toMap(); if (!user.containsKey("deleted_at")) { String userID = getID(user.get(mongoUserID), true); Collection<List<String>> items = Lists.newArrayList(); List<String> item = Lists.newArrayList(); item.add(getID(user.get(mongoItemID), false)); item.add(Float.toString(getPreference(user.get(mongoPreference)))); items.add(item); try { refreshData(userID, items, true); } catch (NoSuchUserException e) { log.warn("No such user ID: {}", userID); } catch (NoSuchItemException e) { log.warn("No such items: {}", items); } if (ts.compareTo(getDate(user.get("created_at"))) < 0) { ts = getDate(user.get("created_at")); } } } if (mongoTimestamp.compareTo(ts) < 0) { mongoTimestamp = ts; } } /** * <p> * Translates the MongoDB identifier to Mahout/MongoDBDataModel's internal * identifier, if required. * </p> * <p> * If MongoDB identifiers are long datatypes, it returns the id. * </p> * <p> * This conversion is needed since Mahout uses the long datatype to feed the * recommender, and MongoDB uses 12 bytes to create its identifiers. * </p> * * @param id MongoDB identifier * @param isUser * @return String containing the translation of the external MongoDB ID to * internal long ID (mapping). * @see #fromLongToId(long) * @see <a href="http://www.mongodb.org/display/DOCS/Object%20IDs"> * Mongo Object IDs</a> */ public String fromIdToLong(String id, boolean isUser) { DBObject objectIdLong = collectionMap.findOne(new BasicDBObject("element_id", id)); if (objectIdLong != null) { Map<String,Object> idLong = (Map<String,Object>) objectIdLong.toMap(); Object value = idLong.get("long_value"); return value == null ? null : value.toString(); } else { objectIdLong = new BasicDBObject(); String longValue = Long.toString(idCounter++); objectIdLong.put("element_id", id); objectIdLong.put("long_value", longValue); collectionMap.insert(objectIdLong); log.info("Adding Translation {}: {} long_value: {}", new Object[] {isUser ? "User ID" : "Item ID", id, longValue}); return longValue; } } /** * <p> * Translates the Mahout/MongoDBDataModel's internal identifier to MongoDB * identifier, if required. * </p> * <p> * If MongoDB identifiers are long datatypes, it returns the id in String * format. * </p> * <p> * This conversion is needed since Mahout uses the long datatype to feed the * recommender, and MongoDB uses 12 bytes to create its identifiers. * </p> * * @param id Mahout's internal identifier * @return String containing the translation of the internal long ID to * external MongoDB ID (mapping). * @see #fromIdToLong(String, boolean) * @see <a href="http://www.mongodb.org/display/DOCS/Object%20IDs"> * Mongo Object IDs</a> */ public String fromLongToId(long id) { DBObject objectIdLong = collectionMap.findOne(new BasicDBObject("long_value", Long.toString(id))); Map<String,Object> idLong = (Map<String,Object>) objectIdLong.toMap(); Object value = idLong.get("element_id"); return value == null ? null : value.toString(); } /** * <p> * Checks if an ID is currently in the model. * </p> * * @param ID user or item ID * @return true: if ID is into the model; false: if it's not. */ public boolean isIDInModel(String ID) { DBObject objectIdLong = collectionMap.findOne(new BasicDBObject("element_id", ID)); return objectIdLong != null; } /** * <p> * Date of the latest update of the model. * </p> * * @return Date with the latest update of the model. */ public Date mongoUpdateDate() { return mongoTimestamp; } private void buildModel() throws UnknownHostException, MongoException { userIsObject = false; itemIsObject = false; idCounter = 0; preferenceIsString = true; Mongo mongoDDBB = new Mongo(mongoHost, mongoPort); DB db = mongoDDBB.getDB(mongoDB); mongoTimestamp = new Date(0); FastByIDMap<Collection<Preference>> userIDPrefMap = new FastByIDMap<Collection<Preference>>(); if (!mongoAuth || (mongoAuth && db.authenticate(mongoUsername, mongoPassword.toCharArray()))) { collection = db.getCollection(mongoCollection); collectionMap = db.getCollection(MONGO_MAP_COLLECTION); DBObject indexObj = new BasicDBObject(); indexObj.put("element_id", 1); collectionMap.ensureIndex(indexObj); indexObj = new BasicDBObject(); indexObj.put("long_value", 1); collectionMap.ensureIndex(indexObj); collectionMap.remove(new BasicDBObject()); DBCursor cursor = collection.find(); while (cursor.hasNext()) { Map<String,Object> user = (Map<String,Object>) cursor.next().toMap(); if (!user.containsKey("deleted_at")) { long userID = Long.parseLong(fromIdToLong(getID(user.get(mongoUserID), true), true)); long itemID = Long.parseLong(fromIdToLong(getID(user.get(mongoItemID), false), false)); float ratingValue = getPreference(user.get(mongoPreference)); Collection<Preference> userPrefs = userIDPrefMap.get(userID); if (userPrefs == null) { userPrefs = Lists.newArrayListWithCapacity(2); userIDPrefMap.put(userID, userPrefs); } userPrefs.add(new GenericPreference(userID, itemID, ratingValue)); if (user.containsKey("created_at") && mongoTimestamp.compareTo(getDate(user.get("created_at"))) < 0) { mongoTimestamp = getDate(user.get("created_at")); } } } } delegate = new GenericDataModel(GenericDataModel.toDataMap(userIDPrefMap, true)); } private void removeMongoUserItem(String userID, String itemID) { String userId = fromLongToId(Long.parseLong(userID)); String itemId = fromLongToId(Long.parseLong(itemID)); if (isUserItemInDB(userId, itemId)) { mongoTimestamp = new Date(); BasicDBObject query = new BasicDBObject(); query.put(mongoUserID, userIsObject ? new ObjectId(userId) : userId); query.put(mongoItemID, itemIsObject ? new ObjectId(itemId) : itemId); if (mongoFinalRemove) { log.info(collection.remove(query).toString()); } else { BasicDBObject update = new BasicDBObject(); update.put("$set", new BasicDBObject("deleted_at", mongoTimestamp)); log.info(collection.update(query, update).toString()); } log.info("Removing userID: {} itemID: {}", userID, itemId); } } private void addMongoUserItem(String userID, String itemID, String preferenceValue) { String userId = fromLongToId(Long.parseLong(userID)); String itemId = fromLongToId(Long.parseLong(itemID)); if (!isUserItemInDB(userId, itemId)) { mongoTimestamp = new Date(); BasicDBObject user = new BasicDBObject(); Object userIdObject = userIsObject ? new ObjectId(userId) : userId; Object itemIdObject = itemIsObject ? new ObjectId(itemId) : itemId; user.put(mongoUserID, userIdObject); user.put(mongoItemID, itemIdObject); user.put(mongoPreference, preferenceIsString ? preferenceValue : Double.parseDouble(preferenceValue)); user.put("created_at", mongoTimestamp); collection.insert(user); log.info("Adding userID: {} itemID: {} preferenceValue: {}", new Object[] {userID, itemID, preferenceValue}); } } private boolean isUserItemInDB(String userID, String itemID) { BasicDBObject query = new BasicDBObject(); Object userId = userIsObject ? new ObjectId(userID) : userID; Object itemId = itemIsObject ? new ObjectId(itemID) : itemID; query.put(mongoUserID, userId); query.put(mongoItemID, itemId); return collection.findOne(query) != null; } private DataModel removeUserItem(long userID, Iterable<List<String>> items) { FastByIDMap<PreferenceArray> rawData = ((GenericDataModel) delegate).getRawUserData(); for (List<String> item : items) { PreferenceArray prefs = rawData.get(userID); long itemID = Long.parseLong(item.get(0)); if (prefs != null) { boolean exists = false; int length = prefs.length(); for (int i = 0; i < length; i++) { if (prefs.getItemID(i) == itemID) { exists = true; break; } } if (exists) { rawData.remove(userID); if (length > 1) { PreferenceArray newPrefs = new GenericUserPreferenceArray(length - 1); for (int i = 0, j = 0; i < length; i++, j++) { if (prefs.getItemID(i) == itemID) { j--; } else { newPrefs.set(j, prefs.get(i)); } } rawData.put(userID, newPrefs); } log.info("Removing userID: {} itemID: {}", userID, itemID); if (mongoManage) { removeMongoUserItem(Long.toString(userID), Long.toString(itemID)); } } } } return new GenericDataModel(rawData); } private DataModel addUserItem(long userID, Iterable<List<String>> items) { FastByIDMap<PreferenceArray> rawData = ((GenericDataModel) delegate).getRawUserData(); PreferenceArray prefs = rawData.get(userID); for (List<String> item : items) { long itemID = Long.parseLong(item.get(0)); float preferenceValue = Float.parseFloat(item.get(1)); boolean exists = false; if (prefs != null) { for (int i = 0; i < prefs.length(); i++) { if (prefs.getItemID(i) == itemID) { exists = true; prefs.setValue(i, preferenceValue); break; } } } if (!exists) { if (prefs == null) { prefs = new GenericUserPreferenceArray(1); } else { PreferenceArray newPrefs = new GenericUserPreferenceArray(prefs.length() + 1); for (int i = 0, j = 1; i < prefs.length(); i++, j++) { newPrefs.set(j, prefs.get(i)); } prefs = newPrefs; } prefs.setUserID(0, userID); prefs.setItemID(0, itemID); prefs.setValue(0, preferenceValue); log.info("Adding userID: {} itemID: {} preferenceValue: {}", new Object[] {userID, itemID, preferenceValue}); rawData.put(userID, prefs); if (mongoManage) { addMongoUserItem(Long.toString(userID), Long.toString(itemID), Float.toString(preferenceValue)); } } } return new GenericDataModel(rawData); } private Date getDate(Object date) { if (date.getClass().getName().contains("Date")) { return (Date) date; } if (date.getClass().getName().contains("String")) { try { synchronized (dateFormat) { return dateFormat.parse(date.toString()); } } catch (ParseException ioe) { log.warn("Error parsing timestamp", ioe); } } return new Date(0); } private float getPreference(Object value) { if (value != null) { if (value.getClass().getName().contains("String")) { preferenceIsString = true; return Float.parseFloat(value.toString()); } else { preferenceIsString = false; return Double.valueOf(value.toString()).floatValue(); } } else { return 0.5f; } } private String getID(Object id, boolean isUser) { if (id.getClass().getName().contains("ObjectId")) { if (isUser) { userIsObject = true; } else { itemIsObject = true; } return ((ObjectId) id).toStringMongod(); } else { return id.toString(); } } private void checkData(String userID, Iterable<List<String>> items, boolean add) throws NoSuchUserException, NoSuchItemException { Preconditions.checkNotNull(userID); Preconditions.checkNotNull(items); Preconditions.checkArgument(!userID.isEmpty()); for (List<String> item : items) { Preconditions.checkNotNull(item.get(0)); Preconditions.checkArgument(!item.get(0).isEmpty()); } if (userIsObject && !ID_PATTERN.matcher(userID).matches()) { throw new IllegalArgumentException(); } for (List<String> item : items) { if (itemIsObject && !ID_PATTERN.matcher(item.get(0)).matches()) { throw new IllegalArgumentException(); } } if (!add && !isIDInModel(userID)) { throw new NoSuchUserException(); } for (List<String> item : items) { if (!add && !isIDInModel(item.get(0))) { throw new NoSuchItemException(); } } } @Override public LongPrimitiveIterator getUserIDs() throws TasteException { return delegate.getUserIDs(); } @Override public PreferenceArray getPreferencesFromUser(long id) throws TasteException { return delegate.getPreferencesFromUser(id); } @Override public FastIDSet getItemIDsFromUser(long userID) throws TasteException { return delegate.getItemIDsFromUser(userID); } @Override public LongPrimitiveIterator getItemIDs() throws TasteException { return delegate.getItemIDs(); } @Override public PreferenceArray getPreferencesForItem(long itemID) throws TasteException { return delegate.getPreferencesForItem(itemID); } @Override public Float getPreferenceValue(long userID, long itemID) throws TasteException { return delegate.getPreferenceValue(userID, itemID); } @Override public Long getPreferenceTime(long userID, long itemID) throws TasteException { return delegate.getPreferenceTime(userID, itemID); } @Override public int getNumItems() throws TasteException { return delegate.getNumItems(); } @Override public int getNumUsers() throws TasteException { return delegate.getNumUsers(); } @Override public int getNumUsersWithPreferenceFor(long itemID) throws TasteException { return delegate.getNumUsersWithPreferenceFor(itemID); } @Override public int getNumUsersWithPreferenceFor(long itemID1, long itemID2) throws TasteException { return delegate.getNumUsersWithPreferenceFor(itemID1, itemID2); } @Override public void setPreference(long userID, long itemID, float value) { throw new UnsupportedOperationException(); } @Override public void removePreference(long userID, long itemID) { throw new UnsupportedOperationException(); } @Override public boolean hasPreferenceValues() { return delegate.hasPreferenceValues(); } @Override public float getMaxPreference() { return delegate.getMaxPreference(); } @Override public float getMinPreference() { return delegate.getMinPreference(); } @Override public String toString() { return "MongoDBDataModel"; } }