package de.saxsys.projectiler.ws.rest; import java.io.IOException; import java.net.UnknownHostException; import java.rmi.ConnectException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import de.saxsys.projectiler.api.Booking; import de.saxsys.projectiler.api.ConnectionException; import de.saxsys.projectiler.api.ProjectileClientApi; import de.saxsys.projectiler.api.ProjectileApiException; import de.saxsys.projectiler.api.Credentials; import de.saxsys.projectiler.api.InvalidCredentialsException; import de.saxsys.projectiler.api.NoBudgetException; import de.saxsys.projectiler.api.Settings; import de.saxsys.projectiler.ws.rest.dto.AddTimebitResponse; import de.saxsys.projectiler.ws.rest.dto.GetTimebitResponse; import de.saxsys.projectiler.ws.rest.dto.Job; import de.saxsys.projectiler.ws.rest.dto.JobsResponse; import de.saxsys.projectiler.ws.rest.dto.LoginCredentials; import de.saxsys.projectiler.ws.rest.dto.Timebit; import okhttp3.HttpUrl; import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Request.Builder; import okhttp3.logging.HttpLoggingInterceptor; import okhttp3.logging.HttpLoggingInterceptor.Level; import retrofit2.Response; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; import retrofit2.converter.scalars.ScalarsConverterFactory; /** * Retrofit implementation of Crawler API. * <p> * Communicates through Projectile REST API. * </p> * * @author stefan.bley */ public class RetrofitClient implements ProjectileClientApi { private static final Logger LOGGER = Logger.getLogger(RetrofitClient.class.getName()); private final Retrofit retrofit; private String token; public RetrofitClient(final Settings settings) { OkHttpClient client = initHttpClient(settings); this.retrofit = new Retrofit.Builder().client(client) .baseUrl(HttpUrl.parse(settings.getProjectileUrl()) .newBuilder() .addPathSegment("rest") .addPathSegment("") // to get a trailing backslash .build()) .addConverterFactory(ScalarsConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create()) .build(); } private OkHttpClient initHttpClient(final Settings settings) { OkHttpClient client = new OkHttpClient.Builder().connectTimeout(settings.getTimeout(), TimeUnit.MILLISECONDS) .readTimeout(settings.getTimeout(), TimeUnit.MILLISECONDS) .addInterceptor(new AuthorizationInterceptor(settings.getApplicationKey())) .addInterceptor(new HttpLoggingInterceptor().setLevel(Level.BODY)) .build(); return client; } @Override public void checkCredentials(Credentials credentials) throws InvalidCredentialsException, ConnectionException { try { login(credentials); } catch (UnknownHostException e) { throw new ConnectionException(e); } catch (IOException e) { throw new ConnectionException(e); } } @Override public List<String> getProjectNames(Credentials credentials) throws ConnectionException, ProjectileApiException { try { login(credentials); return readProjectNames(); } catch (UnknownHostException e) { throw new ConnectionException(e); } catch (IOException e) { throw new ProjectileApiException("Error while retrieving project names."); } } @Override public void clock(Credentials credentials, String projectName, Date start, Date end, String comment) throws ConnectionException, ProjectileApiException { if (null == start || null == end || start.after(end)) { throw new ProjectileApiException("Time has not been clocked. Start time must be before end time."); } try { login(credentials); Job project = lookupProject(projectName); if (null == project) { throw new ProjectileApiException("Time has not been clocked. " + projectName + " is not a valid project."); } else { clockTime(start, end, project, comment); } } catch (UnknownHostException e) { throw new ConnectionException(e); } catch (IOException e) { throw new ProjectileApiException("Error while clocking time."); } } @Override public List<Booking> getDailyReport(Credentials credentials) throws ConnectionException, ProjectileApiException { try { login(credentials); return readDailyReport(); } catch (UnknownHostException e) { throw new ConnectionException(e); } catch (IOException e) { throw new ProjectileApiException("Error while retrieving daily report.", e); } } private void login(Credentials credentials) throws IOException, InvalidCredentialsException { ProjectileRestApi service = retrofit.create(ProjectileRestApi.class); LoginCredentials body = new LoginCredentials(credentials.getUsername(), credentials.getPassword()); Response<String> response = service.getToken(body).execute(); if (response.isSuccessful()) { LOGGER.info("User " + credentials.getUsername() + " logged in."); token = response.body(); } else if (404 == response.code()) { handle404NotFound(); } else if (401 == response.code()) { throw new InvalidCredentialsException(); } } private Job lookupProject(String projectName) throws IOException, ProjectileApiException { List<Job> projects = readProjects(); for (Job project : projects) { if (projectName.equals(project.getProjectName())) { return project; } } return null; } private List<String> readProjectNames() throws IOException, ProjectileApiException { List<String> projectNames = new ArrayList<>(); List<Job> projects = readProjects(); for (Job project : projects) { projectNames.add(project.getProjectName()); } LOGGER.info("Projects: " + projectNames); return projectNames; } private List<Job> readProjects() throws IOException, ProjectileApiException { ProjectileRestApi service = retrofit.create(ProjectileRestApi.class); Response<JobsResponse> response = service.getJobs().execute(); if (response.isSuccessful()) { LOGGER.info("Projects read."); return response.body().getJobs(); } else if (404 == response.code()) { handle404NotFound(); } else if (401 == response.code()) { throw new InvalidCredentialsException(); } else { LOGGER.severe(response.errorBody().string()); } throw new IOException(); } private Job readProject(String jobId) throws IOException, ProjectileApiException { ProjectileRestApi service = retrofit.create(ProjectileRestApi.class); Response<JobsResponse> response = service.getJob(jobId).execute(); if (response.isSuccessful()) { LOGGER.info("Project " + jobId + " read."); JobsResponse body = response.body(); if (body.getStatusCode().isError()) { handle404NotFound(); } else { return body.getJobs().get(0); } } else if (404 == response.code()) { handle404NotFound(); } else if (401 == response.code()) { throw new InvalidCredentialsException(); } else { LOGGER.severe(response.errorBody().string()); } throw new IOException(); } private Object clockTime(Date start, Date end, Job project, String comment) throws ProjectileApiException, IOException { if (null == start || null == end || null == project) { LOGGER.warning("Time has not been clocked. Start time, end time or project missing."); throw new ProjectileApiException("Time has not been clocked. Start time, end time or project missing."); } ProjectileRestApi service = retrofit.create(ProjectileRestApi.class); Timebit timebit = new Timebit(new SimpleDateFormat("yyyy-MM-dd").format(new Date()), formatTime(start), formatTime(end), project.getId(), comment); Response<AddTimebitResponse> response = service.addTimebit(timebit).execute(); if (response.isSuccessful()) { AddTimebitResponse addTimebitResponse = response.body(); if (addTimebitResponse.getStatusCode().isError()) { if (7 == addTimebitResponse.getStatusCode().getCodeNumber()) { throw new NoBudgetException(); } else { throw new ProjectileApiException(addTimebitResponse.getMessage()); } } else if (addTimebitResponse.getStatusCode().isWarning()) { LOGGER.warning("Time clocked with warning: " + addTimebitResponse.getMessage()); } LOGGER.info("Time clocked for project '" + project.getProjectName() + "'."); } else if (404 == response.code()) { handle404NotFound(); } else if (401 == response.code()) { throw new InvalidCredentialsException(); } return null; } private List<Booking> readDailyReport() throws IOException, InvalidCredentialsException { Calendar today = Calendar.getInstance(); String startDate = new SimpleDateFormat("yyyy-MM-dd").format(today.getTime()); ProjectileRestApi service = retrofit.create(ProjectileRestApi.class); Response<GetTimebitResponse> response = service.getTimebits(startDate, startDate).execute(); if (response.isSuccessful()) { LOGGER.info("Daily report read."); List<Timebit> timebits = response.body().getTimebits(); List<Booking> bookings = new ArrayList<>(); for (Timebit timebit : timebits) { String projectName = ""; try { Job project = readProject(timebit.getJobId()); projectName = project.getProjectName(); } catch (ProjectileApiException e) { LOGGER.warning("Project name for job " + timebit.getJobId() + " could not be read."); } bookings.add(new Booking(timebit.getJobId(), projectName, timebit.getStarttime(), timebit.getEndtime(), timebit.getNote())); } return bookings; } else if (404 == response.code()) { handle404NotFound(); } else if (401 == response.code()) { throw new InvalidCredentialsException(); } else { LOGGER.severe(response.errorBody().string()); } throw new IOException(); } private void handle404NotFound() throws ConnectException { throw new ConnectException("Invalid Projectile URL."); } /** Time formatted to Projectile format */ private String formatTime(final Date time) { return new SimpleDateFormat("HH:mm").format(time); } private final class AuthorizationInterceptor implements Interceptor { private String applicationKey; private AuthorizationInterceptor(String applicationKey) { this.applicationKey = applicationKey; } @Override public okhttp3.Response intercept(Chain chain) throws IOException { Builder requestBuilder = chain.request().newBuilder(); requestBuilder.header("x-application-key", applicationKey); LOGGER.info("Access token: " + token); if (null != token) { requestBuilder.header("Authorization", "Bearer " + token); } return chain.proceed(requestBuilder.build()); } } }