/** * 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.waveprotocol.box.server.persistence.mongodb; import com.google.protobuf.InvalidProtocolBufferException; import com.google.wave.api.Context; import com.google.wave.api.ProtocolVersion; import com.google.wave.api.event.EventType; import com.google.wave.api.robot.Capability; import com.mongodb.BasicDBObject; import com.mongodb.DB; import com.mongodb.DBCollection; import com.mongodb.DBObject; import com.mongodb.MongoException; import com.mongodb.gridfs.GridFS; import com.mongodb.gridfs.GridFSDBFile; import com.mongodb.gridfs.GridFSInputFile; import org.bson.types.BasicBSONList; import org.waveprotocol.box.attachment.AttachmentMetadata; import org.waveprotocol.box.attachment.AttachmentProto; import org.waveprotocol.box.attachment.proto.AttachmentMetadataProtoImpl; import org.waveprotocol.box.server.account.AccountData; import org.waveprotocol.box.server.account.HumanAccountData; import org.waveprotocol.box.server.account.HumanAccountDataImpl; import org.waveprotocol.box.server.account.RobotAccountData; import org.waveprotocol.box.server.account.RobotAccountDataImpl; import org.waveprotocol.box.server.authentication.PasswordDigest; import org.waveprotocol.box.server.persistence.AccountStore; import org.waveprotocol.box.server.persistence.AttachmentStore; import org.waveprotocol.box.server.persistence.PersistenceException; import org.waveprotocol.box.server.persistence.SignerInfoStore; import org.waveprotocol.box.server.robots.RobotCapabilities; import org.waveprotocol.wave.crypto.SignatureException; import org.waveprotocol.wave.crypto.SignerInfo; import org.waveprotocol.wave.federation.Proto.ProtocolSignerInfo; import org.waveprotocol.wave.media.model.AttachmentId; import org.waveprotocol.wave.model.util.CollectionUtils; import org.waveprotocol.wave.model.wave.ParticipantId; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.logging.Level; import java.util.logging.Logger; /** * <b>CertPathStore:</b><br/> * <i>Collection(signerInfo):</i> * <ul> * <li>_id : signerId byte array.</li> * <li>protoBuff : byte array representing the protobuff message of a * {@link ProtocolSignerInfo}.</li> * </ul> * <p> * * @author ljvderijk@google.com (Lennard de Rijk) * @author josephg@gmail.com (Joseph Gentle) * */ public final class MongoDbStore implements SignerInfoStore, AttachmentStore, AccountStore { private static final String ACCOUNT_COLLECTION = "account"; private static final String ACCOUNT_HUMAN_DATA_FIELD = "human"; private static final String ACCOUNT_ROBOT_DATA_FIELD = "robot"; private static final String HUMAN_PASSWORD_FIELD = "passwordDigest"; private static final String PASSWORD_DIGEST_FIELD = "digest"; private static final String PASSWORD_SALT_FIELD = "salt"; private static final String ROBOT_URL_FIELD = "url"; private static final String ROBOT_SECRET_FIELD = "secret"; private static final String ROBOT_CAPABILITIES_FIELD = "capabilities"; private static final String ROBOT_VERIFIED_FIELD = "verified"; private static final String CAPABILITIES_VERSION_FIELD = "version"; private static final String CAPABILITIES_HASH_FIELD = "capabilitiesHash"; private static final String CAPABILITIES_CAPABILITIES_FIELD = "capabilities"; private static final String CAPABILITY_CONTEXTS_FIELD = "contexts"; private static final String CAPABILITY_FILTER_FIELD = "filter"; private static final Logger LOG = Logger.getLogger(MongoDbStore.class.getName()); private final DB database; private final GridFS attachmentGrid; private final GridFS thumbnailGrid; private final GridFS metadataGrid; MongoDbStore(DB database) { this.database = database; attachmentGrid = new GridFS(database, "attachments"); thumbnailGrid = new GridFS(database, "thumbnails"); metadataGrid = new GridFS(database, "metadata"); } @Override public void initializeSignerInfoStore() throws PersistenceException { // Nothing to initialize } @Override public SignerInfo getSignerInfo(byte[] signerId) { DBObject query = getDBObjectForSignerId(signerId); DBCollection signerInfoCollection = getSignerInfoCollection(); DBObject signerInfoDBObject = signerInfoCollection.findOne(query); // Sub-class contract specifies return null when not found SignerInfo signerInfo = null; if (signerInfoDBObject != null) { byte[] protobuff = (byte[]) signerInfoDBObject.get("protoBuff"); try { signerInfo = new SignerInfo(ProtocolSignerInfo.parseFrom(protobuff)); } catch (InvalidProtocolBufferException e) { LOG.log(Level.SEVERE, "Couldn't parse the protobuff stored in MongoDB: " + protobuff, e); } catch (SignatureException e) { LOG.log(Level.SEVERE, "Couldn't parse the certificate chain or domain properly", e); } } return signerInfo; } @Override public void putSignerInfo(ProtocolSignerInfo protocolSignerInfo) throws SignatureException { SignerInfo signerInfo = new SignerInfo(protocolSignerInfo); byte[] signerId = signerInfo.getSignerId(); // Not using a modifier here because rebuilding the object is not a lot of // work. Doing implicit upsert by using save with a DBOBject that has an _id // set. DBObject signerInfoDBObject = getDBObjectForSignerId(signerId); signerInfoDBObject.put("protoBuff", protocolSignerInfo.toByteArray()); getSignerInfoCollection().save(signerInfoDBObject); } /** * Returns an instance of {@link DBCollection} for storing SignerInfo. */ private DBCollection getSignerInfoCollection() { return database.getCollection("signerInfo"); } /** * Returns a {@link DBObject} which contains the key-value pair used to * signify the signerId. * * @param signerId the signerId value to set. * @return a new {@link DBObject} with the (_id,signerId) entry. */ private DBObject getDBObjectForSignerId(byte[] signerId) { DBObject query = new BasicDBObject(); query.put("_id", signerId); return query; } // *********** Attachments. @Override public AttachmentData getAttachment(AttachmentId attachmentId) { final GridFSDBFile attachment = attachmentGrid.findOne(attachmentId.serialise()); return fileToAttachmentData(attachment); } @Override public void storeAttachment(AttachmentId attachmentId, InputStream data) throws IOException { saveFile(attachmentGrid.createFile(data, attachmentId.serialise())); } @Override public void deleteAttachment(AttachmentId attachmentId) { attachmentGrid.remove(attachmentId.serialise()); thumbnailGrid.remove(attachmentId.serialise()); metadataGrid.remove(attachmentId.serialise()); } @Override public AttachmentMetadata getMetadata(AttachmentId attachmentId) throws IOException { final GridFSDBFile metadata = metadataGrid.findOne(attachmentId.serialise()); if (metadata == null) { return null; } AttachmentProto.AttachmentMetadata protoMetadata = AttachmentProto.AttachmentMetadata.parseFrom(metadata.getInputStream()); return new AttachmentMetadataProtoImpl(protoMetadata); } @Override public AttachmentData getThumbnail(AttachmentId attachmentId) throws IOException { final GridFSDBFile thumbnail = thumbnailGrid.findOne(attachmentId.serialise()); return fileToAttachmentData(thumbnail); } @Override public void storeMetadata(AttachmentId attachmentId, AttachmentMetadata metaData) throws IOException { AttachmentMetadataProtoImpl proto = new AttachmentMetadataProtoImpl(metaData); byte[] bytes = proto.getPB().toByteArray(); GridFSInputFile file = metadataGrid.createFile(new ByteArrayInputStream(bytes), attachmentId.serialise()); saveFile(file); } @Override public void storeThumbnail(AttachmentId attachmentId, InputStream dataData) throws IOException { saveFile(thumbnailGrid.createFile(dataData, attachmentId.serialise())); } private void saveFile(GridFSInputFile file) throws IOException { try { file.save(); } catch (MongoException e) { // Unfortunately, file.save() wraps any IOException thrown in a // 'MongoException'. Since the interface explicitly throws IOExceptions, // we unwrap any IOExceptions thrown. Throwable innerException = e.getCause(); if (innerException instanceof IOException) { throw (IOException) innerException; } else { throw e; } }; } private AttachmentData fileToAttachmentData(final GridFSDBFile attachmant) { if (attachmant == null) { return null; } else { return new AttachmentData() { @Override public InputStream getInputStream() throws IOException { return attachmant.getInputStream(); } @Override public long getSize() { return attachmant.getLength(); } }; } } // ******** AccountStore @Override public void initializeAccountStore() throws PersistenceException { // TODO: Sanity checks not handled by MongoDBProvider??? } @Override public AccountData getAccount(ParticipantId id) { DBObject query = getDBObjectForParticipant(id); DBObject result = getAccountCollection().findOne(query); if (result == null) { return null; } DBObject human = (DBObject) result.get(ACCOUNT_HUMAN_DATA_FIELD); if (human != null) { return objectToHuman(id, human); } DBObject robot = (DBObject) result.get(ACCOUNT_ROBOT_DATA_FIELD); if (robot != null) { return objectToRobot(id, robot); } throw new IllegalStateException("DB object contains neither a human nor a robot"); } @Override public void putAccount(AccountData account) { DBObject object = getDBObjectForParticipant(account.getId()); if (account.isHuman()) { object.put(ACCOUNT_HUMAN_DATA_FIELD, humanToObject(account.asHuman())); } else if (account.isRobot()) { object.put(ACCOUNT_ROBOT_DATA_FIELD, robotToObject(account.asRobot())); } else { throw new IllegalStateException("Account is neither a human nor a robot"); } getAccountCollection().save(object); } @Override public void removeAccount(ParticipantId id) { DBObject object = getDBObjectForParticipant(id); getAccountCollection().remove(object); } private DBObject getDBObjectForParticipant(ParticipantId id) { DBObject query = new BasicDBObject(); query.put("_id", id.getAddress()); return query; } private DBCollection getAccountCollection() { return database.getCollection(ACCOUNT_COLLECTION); } // ****** HumanAccountData serialization private DBObject humanToObject(HumanAccountData account) { DBObject object = new BasicDBObject(); PasswordDigest digest = account.getPasswordDigest(); if (digest != null) { DBObject digestObj = new BasicDBObject(); digestObj.put(PASSWORD_SALT_FIELD, digest.getSalt()); digestObj.put(PASSWORD_DIGEST_FIELD, digest.getDigest()); object.put(HUMAN_PASSWORD_FIELD, digestObj); } return object; } private HumanAccountData objectToHuman(ParticipantId id, DBObject object) { PasswordDigest passwordDigest = null; DBObject digestObj = (DBObject) object.get(HUMAN_PASSWORD_FIELD); if (digestObj != null) { byte[] salt = (byte[]) digestObj.get(PASSWORD_SALT_FIELD); byte[] digest = (byte[]) digestObj.get(PASSWORD_DIGEST_FIELD); passwordDigest = PasswordDigest.from(salt, digest); } return new HumanAccountDataImpl(id, passwordDigest); } // ****** RobotAccountData serialization private DBObject robotToObject(RobotAccountData account) { return new BasicDBObject() .append(ROBOT_URL_FIELD, account.getUrl()) .append(ROBOT_SECRET_FIELD, account.getConsumerSecret()) .append(ROBOT_CAPABILITIES_FIELD, capabilitiesToObject(account.getCapabilities())) .append(ROBOT_VERIFIED_FIELD, account.isVerified()); } private DBObject capabilitiesToObject(RobotCapabilities capabilities) { if (capabilities == null) { return null; } BasicDBObject capabilitiesObj = new BasicDBObject(); for (Capability capability : capabilities.getCapabilitiesMap().values()) { BasicBSONList contexts = new BasicBSONList(); for (Context c : capability.getContexts()) { contexts.add(c.name()); } capabilitiesObj.put(capability.getEventType().name(), new BasicDBObject() .append(CAPABILITY_CONTEXTS_FIELD, contexts) .append(CAPABILITY_FILTER_FIELD, capability.getFilter())); } BasicDBObject object = new BasicDBObject() .append(CAPABILITIES_CAPABILITIES_FIELD, capabilitiesObj) .append(CAPABILITIES_HASH_FIELD, capabilities.getCapabilitiesHash()) .append(CAPABILITIES_VERSION_FIELD, capabilities.getProtocolVersion().name()); return object; } private AccountData objectToRobot(ParticipantId id, DBObject robot) { String url = (String) robot.get(ROBOT_URL_FIELD); String secret = (String) robot.get(ROBOT_SECRET_FIELD); RobotCapabilities capabilities = objectToCapabilities((DBObject) robot.get(ROBOT_CAPABILITIES_FIELD)); boolean verified = (Boolean) robot.get(ROBOT_VERIFIED_FIELD); return new RobotAccountDataImpl(id, url, secret, capabilities, verified); } @SuppressWarnings("unchecked") private RobotCapabilities objectToCapabilities(DBObject object) { if (object == null) { return null; } Map<String, Object> capabilitiesObj = (Map<String, Object>) object.get(CAPABILITIES_CAPABILITIES_FIELD); Map<EventType, Capability> capabilities = CollectionUtils.newHashMap(); for (Entry<String, Object> capability : capabilitiesObj.entrySet()) { EventType eventType = EventType.valueOf(capability.getKey()); List<Context> contexts = CollectionUtils.newArrayList(); DBObject capabilityObj = (DBObject) capability.getValue(); DBObject contextsObj = (DBObject) capabilityObj.get(CAPABILITY_CONTEXTS_FIELD); for (String contextId : contextsObj.keySet()) { contexts.add(Context.valueOf((String) contextsObj.get(contextId))); } String filter = (String) capabilityObj.get(CAPABILITY_FILTER_FIELD); capabilities.put(eventType, new Capability(eventType, contexts, filter)); } String capabilitiesHash = (String) object.get(CAPABILITIES_HASH_FIELD); ProtocolVersion version = ProtocolVersion.valueOf((String) object.get(CAPABILITIES_VERSION_FIELD)); return new RobotCapabilities(capabilities, capabilitiesHash, version); } }