/**
* 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.robots.register;
import com.google.common.base.Preconditions;
import com.google.inject.Inject;
import org.waveprotocol.box.server.account.AccountData;
import org.waveprotocol.box.server.account.RobotAccountData;
import org.waveprotocol.box.server.account.RobotAccountDataImpl;
import org.waveprotocol.box.server.persistence.AccountStore;
import org.waveprotocol.box.server.persistence.PersistenceException;
import org.waveprotocol.box.server.robots.util.RobotsUtil.RobotRegistrationException;
import org.waveprotocol.wave.model.id.TokenGenerator;
import org.waveprotocol.wave.model.wave.ParticipantId;
import java.net.URI;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.logging.Logger;
/**
* Implements {@link RobotRegistrar}.
*
* @author yurize@apache.org (Yuri Zelikov)
*/
public class RobotRegistrarImpl implements RobotRegistrar {
private static final Listener REGISTRATION_EVENTS_LOGGER = new Listener() {
final Logger log = Logger.getLogger(RobotRegistrarImpl.class.getName());
@Override
public void onRegistrationSuccess(RobotAccountData account) {
log.info("Registered robot: " + account.getId().getAddress() + " at " + account.getUrl());
}
@Override
public void onUnregistrationSuccess(RobotAccountData account) {
log.info("Unregistered robot: " + account.getId().getAddress() + " at " + account.getUrl());
}
};
/** The length of the verification token (token secret). */
private static final int TOKEN_LENGTH = 48;
/** The account store. */
private final AccountStore accountStore;
/** The verification token generator. */
private final TokenGenerator tokenGenerator;
/** The list of listeners on robot un/registration events. */
private final CopyOnWriteArraySet<Listener> listeners = new CopyOnWriteArraySet<Listener>();
/**
* Computes and validates the robot URL.
*
* @param location the robot location.
* @return the validated robot URL in the form:
* [http|https]://[domain]:[port]/[path], for example:
* http://example.com:80/myrobot
* @throws RobotRegistrationException if the specified URI is invalid.
*/
private static String computeValidateRobotUrl(String location)
throws RobotRegistrationException {
URI uri;
try {
uri = URI.create(location);
} catch (IllegalArgumentException e) {
String errorMessage = "Invalid Location specified, please specify a location in URI format.";
throw new RobotRegistrationException(errorMessage + " " + e.getLocalizedMessage(), e);
}
String scheme = uri.getScheme();
if (scheme == null || (!scheme.equals("http") && !scheme.equals("https"))) {
scheme = "http";
}
String robotLocation;
if (uri.getPort() != -1) {
robotLocation = scheme + "://" + uri.getHost() + ":" + uri.getPort() + uri.getPath();
} else {
robotLocation = scheme + "://" + uri.getHost() + uri.getPath();
}
if (robotLocation.endsWith("/")) {
robotLocation = robotLocation.substring(0, robotLocation.length() - 1);
}
return robotLocation;
}
@Inject
public RobotRegistrarImpl(AccountStore accountStore, TokenGenerator tokenGenerator) {
this.accountStore = accountStore;
this.tokenGenerator = tokenGenerator;
addRegistrationListener(REGISTRATION_EVENTS_LOGGER);
}
@Override
public RobotAccountData registerNew(ParticipantId robotId, String location)
throws RobotRegistrationException, PersistenceException {
Preconditions.checkNotNull(robotId);
Preconditions.checkNotNull(location);
Preconditions.checkArgument(!location.isEmpty());
if (accountStore.getAccount(robotId) != null) {
throw new RobotRegistrationException(robotId.getAddress()
+ " is already in use, please choose another one.");
}
return registerRobot(robotId, location);
}
@Override
public RobotAccountData unregister(ParticipantId robotId) throws RobotRegistrationException,
PersistenceException {
Preconditions.checkNotNull(robotId);
AccountData accountData = accountStore.getAccount(robotId);
if (accountData == null) {
return null;
}
throwExceptionIfNotRobot(accountData);
RobotAccountData robotAccount = accountData.asRobot();
removeRobotAccount(robotAccount);
return robotAccount;
}
@Override
public RobotAccountData registerOrUpdate(ParticipantId robotId, String location)
throws RobotRegistrationException, PersistenceException {
Preconditions.checkNotNull(robotId);
Preconditions.checkNotNull(location);
Preconditions.checkArgument(!location.isEmpty());
AccountData account = accountStore.getAccount(robotId);
if (account != null) {
throwExceptionIfNotRobot(account);
RobotAccountData robotAccount = account.asRobot();
if (robotAccount.getUrl().equals(location)) {
return robotAccount;
} else {
removeRobotAccount(robotAccount);
}
}
return registerRobot(robotId, location);
}
/**
* Adds the robot to the account store and notifies the listeners.
*/
private RobotAccountData registerRobot(ParticipantId robotId, String location)
throws RobotRegistrationException, PersistenceException {
String robotLocation = computeValidateRobotUrl(location);
RobotAccountData robotAccount =
new RobotAccountDataImpl(robotId, robotLocation,
tokenGenerator.generateToken(TOKEN_LENGTH), null, true);
accountStore.putAccount(robotAccount);
for (Listener listener : listeners) {
listener.onRegistrationSuccess(robotAccount);
}
return robotAccount;
}
/**
* Removes the robot account and notifies the listeners.
* @param existingAccount the account to remove
* @throws PersistenceException if the persistence layer reports an error.
*/
private void removeRobotAccount(RobotAccountData existingAccount)
throws PersistenceException {
accountStore.removeAccount(existingAccount.getId());
for (Listener listener : listeners) {
listener.onUnregistrationSuccess(existingAccount);
}
}
/**
* Ensures that the account belongs to a robot.
*
* @param existingAccount the account to check.
* @throws RobotRegistrationException if the account is not robot.
*/
private void throwExceptionIfNotRobot(AccountData existingAccount)
throws RobotRegistrationException {
if (!existingAccount.isRobot()) {
throw new RobotRegistrationException(existingAccount.getId().getAddress()
+ " is not a robot account!");
}
}
// Handle listeners.
@Override
public void addRegistrationListener(Listener listener) {
Preconditions.checkNotNull(listener);
listeners.add(listener);
}
@Override
public void removeRegistrationListener(Listener listener) {
Preconditions.checkNotNull(listener);
listeners.remove(listener);
}
}