parent
00b62e4167
commit
fa1f2fb30d
@ -1,154 +0,0 @@ |
||||
package de.kreth.clubhelperbackend.google; |
||||
|
||||
import java.io.File; |
||||
import java.io.FileInputStream; |
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
import java.io.InputStreamReader; |
||||
import java.nio.charset.Charset; |
||||
import java.security.GeneralSecurityException; |
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
import java.util.Objects; |
||||
|
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
|
||||
import com.google.api.client.auth.oauth2.Credential; |
||||
import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp; |
||||
import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver; |
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow; |
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets; |
||||
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; |
||||
import com.google.api.client.http.HttpTransport; |
||||
import com.google.api.client.json.JsonFactory; |
||||
import com.google.api.client.json.jackson2.JacksonFactory; |
||||
import com.google.api.client.util.store.FileDataStoreFactory; |
||||
import com.google.api.services.calendar.Calendar.Builder; |
||||
import com.google.api.services.calendar.CalendarScopes; |
||||
|
||||
public abstract class GoogleBaseAdapter { |
||||
|
||||
private static final int GOOGLE_SECRET_PORT = 59431; |
||||
|
||||
/** Application name. */ |
||||
protected static final String APPLICATION_NAME = "ClubHelperVaadin"; |
||||
/** Directory to store user credentials for this application. */ |
||||
protected static final File DATA_STORE_DIR = new File(System.getProperty("catalina.base"), ".credentials"); |
||||
/** Global instance of the JSON factory. */ |
||||
protected static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance(); |
||||
/** |
||||
* Global instance of the scopes required by this quickstart. |
||||
* |
||||
* If modifying these scopes, delete your previously saved credentials |
||||
*/ |
||||
static final List<String> SCOPES = Arrays.asList(CalendarScopes.CALENDAR); |
||||
|
||||
private static Credential credential; |
||||
|
||||
protected final Logger log = LoggerFactory.getLogger(getClass()); |
||||
/** Global instance of the {@link FileDataStoreFactory}. */ |
||||
protected final FileDataStoreFactory DATA_STORE_FACTORY; |
||||
/** Global instance of the HTTP transport. */ |
||||
protected final HttpTransport HTTP_TRANSPORT; |
||||
|
||||
public GoogleBaseAdapter() throws GeneralSecurityException, IOException { |
||||
super(); |
||||
HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport(); |
||||
DATA_STORE_FACTORY = new FileDataStoreFactory(DATA_STORE_DIR); |
||||
if (DATA_STORE_DIR.mkdirs()) { |
||||
log.info("created DATA_STORE_DIR: {}", DATA_STORE_DIR.getAbsolutePath()); |
||||
} else { |
||||
log.trace("DATA_STORE_DIR already exists."); |
||||
} |
||||
} |
||||
|
||||
protected void checkRefreshToken(String serverName) throws IOException { |
||||
synchronized (SCOPES) { |
||||
if (credential == null) { |
||||
credential = Objects.requireNonNull(authorize(serverName)); |
||||
} |
||||
} |
||||
|
||||
if ((credential.getExpiresInSeconds() != null && credential.getExpiresInSeconds() < 3600)) { |
||||
|
||||
if (log.isDebugEnabled()) { |
||||
log.debug("Security needs refresh, trying."); |
||||
} |
||||
boolean result = credential.refreshToken(); |
||||
if (log.isDebugEnabled()) { |
||||
log.debug("Token refresh {}", (result ? "successfull." : "failed.")); |
||||
} |
||||
} |
||||
} |
||||
|
||||
public Builder createBuilder() { |
||||
if (credential == null) { |
||||
throw new IllegalStateException("credential is null, checkRefreshToken need to be called before."); |
||||
} |
||||
|
||||
return new com.google.api.services.calendar.Calendar.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential); |
||||
} |
||||
|
||||
public final boolean refreshToken() throws IOException { |
||||
return credential.refreshToken(); |
||||
} |
||||
|
||||
/** |
||||
* Creates an authorized Credential object. |
||||
* |
||||
* @param request |
||||
* |
||||
* @return an authorized Credential object. |
||||
* @throws IOException |
||||
*/ |
||||
private Credential authorize(String serverName) throws IOException { |
||||
if (credential != null |
||||
&& (credential.getExpiresInSeconds() != null && credential.getExpiresInSeconds() < 3600)) { |
||||
credential.refreshToken(); |
||||
return credential; |
||||
} |
||||
|
||||
// Load client secrets.
|
||||
InputStream in = getClass().getResourceAsStream("/client_secret.json"); |
||||
if (in == null) { |
||||
File inHome = new File(System.getProperty("user.home"), "client_secret.json"); |
||||
if (inHome.exists()) { |
||||
if (log.isInfoEnabled()) { |
||||
log.info("Google secret not found as Resource, using user Home file."); |
||||
} |
||||
in = new FileInputStream(inHome); |
||||
} else { |
||||
log.error("Failed to load client_secret.json. Download from google console."); |
||||
return null; |
||||
} |
||||
} |
||||
GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY, |
||||
new InputStreamReader(in, Charset.defaultCharset())); |
||||
if (log.isTraceEnabled()) { |
||||
log.trace("client secret json resource loaded."); |
||||
} |
||||
|
||||
// Build flow and trigger user authorization request.
|
||||
GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(HTTP_TRANSPORT, JSON_FACTORY, |
||||
clientSecrets, SCOPES).setDataStoreFactory(DATA_STORE_FACTORY).setAccessType("offline") |
||||
.setApprovalPrompt("force").build(); |
||||
|
||||
if (log.isDebugEnabled()) { |
||||
log.debug("Configuring google LocalServerReceiver on {}: {}", serverName, GOOGLE_SECRET_PORT); |
||||
} |
||||
|
||||
LocalServerReceiver localServerReceiver = new LocalServerReceiver.Builder().setHost(serverName) |
||||
.setPort(GOOGLE_SECRET_PORT).build(); |
||||
|
||||
Credential auth = new AuthorizationCodeInstalledApp(flow, localServerReceiver).authorize("user"); |
||||
if (log.isDebugEnabled()) { |
||||
log.debug("Credentials saved to " + DATA_STORE_DIR.getAbsolutePath()); |
||||
} |
||||
|
||||
auth.setExpiresInSeconds(Long.valueOf(691200L)); |
||||
|
||||
return auth; |
||||
} |
||||
|
||||
} |
||||
@ -1,175 +0,0 @@ |
||||
|
||||
package de.kreth.clubhelperbackend.google.calendar; |
||||
|
||||
import java.io.IOException; |
||||
import java.security.GeneralSecurityException; |
||||
import java.util.ArrayList; |
||||
import java.util.GregorianCalendar; |
||||
import java.util.List; |
||||
import java.util.concurrent.ExecutorService; |
||||
import java.util.concurrent.Executors; |
||||
import java.util.concurrent.TimeUnit; |
||||
import java.util.concurrent.locks.Lock; |
||||
import java.util.concurrent.locks.ReentrantLock; |
||||
|
||||
import org.springframework.stereotype.Component; |
||||
|
||||
import com.google.api.client.util.DateTime; |
||||
import com.google.api.services.calendar.model.Calendar; |
||||
import com.google.api.services.calendar.model.CalendarList; |
||||
import com.google.api.services.calendar.model.CalendarListEntry; |
||||
import com.google.api.services.calendar.model.Event; |
||||
import com.google.api.services.calendar.model.EventAttendee; |
||||
|
||||
import de.kreth.clubhelperbackend.google.GoogleBaseAdapter; |
||||
import de.kreth.clubhelperbackend.google.calendar.CalendarResource.CalendarKonfig; |
||||
|
||||
@Component |
||||
public class CalendarAdapter extends GoogleBaseAdapter { |
||||
|
||||
com.google.api.services.calendar.Calendar service; |
||||
private Lock lock = new ReentrantLock(); |
||||
private CalendarResource res; |
||||
|
||||
public CalendarAdapter() throws GeneralSecurityException, IOException { |
||||
super(); |
||||
res = new CalendarResource(); |
||||
|
||||
} |
||||
|
||||
@Override |
||||
protected void checkRefreshToken(String serverName) throws IOException { |
||||
try { |
||||
if (lock.tryLock(10, TimeUnit.SECONDS)) { |
||||
try { |
||||
super.checkRefreshToken(serverName); |
||||
if (service == null) { |
||||
service = createBuilder().setApplicationName(APPLICATION_NAME).build(); |
||||
} |
||||
} finally { |
||||
lock.unlock(); |
||||
} |
||||
} |
||||
} catch (InterruptedException e) { |
||||
log.warn("Lock interrupted", e); |
||||
if (service == null) { |
||||
throw new IOException("Unable to create Service", e); |
||||
} |
||||
} |
||||
if (service == null) { |
||||
throw new IllegalStateException("Calendar Service not instanciated!"); |
||||
} |
||||
} |
||||
|
||||
Calendar getCalendarBySummaryName(List<CalendarListEntry> items, String calendarSummary) throws IOException { |
||||
|
||||
String calendarId = null; |
||||
for (CalendarListEntry e : items) { |
||||
if (calendarSummary.equals(e.getSummary())) { |
||||
calendarId = e.getId(); |
||||
break; |
||||
} |
||||
} |
||||
if (calendarId == null) { |
||||
throw new IllegalStateException("Calendar " + calendarSummary + " not found!"); |
||||
} |
||||
|
||||
return service.calendars().get(calendarId).execute(); |
||||
} |
||||
|
||||
public List<Event> getAllEvents(String serverName) throws IOException, InterruptedException { |
||||
|
||||
final List<Event> events = new ArrayList<>(); |
||||
|
||||
List<CalendarListEntry> items = getCalendarList(serverName); |
||||
final long oldest = getOldest(); |
||||
|
||||
List<CalendarKonfig> configs = res.getConfigs(); |
||||
ExecutorService exec = Executors.newFixedThreadPool(configs.size()); |
||||
for (CalendarKonfig c : configs) { |
||||
exec.execute(new FetchEventsRunner(items, c.getName(), events, oldest, c.getColor())); |
||||
} |
||||
|
||||
exec.shutdown(); |
||||
try { |
||||
exec.awaitTermination(20, TimeUnit.SECONDS); |
||||
} catch (InterruptedException e) { |
||||
log.error("Thread terminated - event list may be incomplete.", e); |
||||
} |
||||
return events; |
||||
} |
||||
|
||||
private long getOldest() { |
||||
GregorianCalendar oldestCal = new GregorianCalendar(); |
||||
oldestCal.set(java.util.Calendar.DAY_OF_MONTH, 1); |
||||
oldestCal.set(java.util.Calendar.HOUR_OF_DAY, 0); |
||||
oldestCal.set(java.util.Calendar.MINUTE, 0); |
||||
oldestCal.add(java.util.Calendar.MONTH, -1); |
||||
oldestCal.add(java.util.Calendar.YEAR, -1); |
||||
oldestCal.add(java.util.Calendar.HOUR_OF_DAY, -1); |
||||
|
||||
return oldestCal.getTimeInMillis(); |
||||
} |
||||
|
||||
List<CalendarListEntry> getCalendarList(String serverName) throws IOException { |
||||
checkRefreshToken(serverName); |
||||
CalendarList calendarList; |
||||
try { |
||||
calendarList = service.calendarList().list().execute(); |
||||
} catch (IOException e) { |
||||
if (log.isWarnEnabled()) { |
||||
log.warn("Error fetching Calendar List, trying token refresh", e); |
||||
} |
||||
refreshToken(); |
||||
if (log.isInfoEnabled()) { |
||||
log.info("Successfully refreshed Google Security Token."); |
||||
} |
||||
calendarList = service.calendarList().list().execute(); |
||||
} |
||||
return calendarList.getItems(); |
||||
} |
||||
|
||||
private final class FetchEventsRunner implements Runnable { |
||||
private final List<Event> events; |
||||
private final long oldest; |
||||
private String colorClass; |
||||
private List<CalendarListEntry> items; |
||||
private String summary; |
||||
|
||||
private FetchEventsRunner(List<CalendarListEntry> items, String summary, List<Event> events, long oldest, |
||||
String colorClass) { |
||||
this.events = events; |
||||
this.oldest = oldest; |
||||
this.items = items; |
||||
this.summary = summary; |
||||
this.colorClass = colorClass; |
||||
} |
||||
|
||||
@Override |
||||
public void run() { |
||||
|
||||
try { |
||||
log.debug("Fetching events of calendar \"{}\"", summary); |
||||
Calendar calendar = getCalendarBySummaryName(items, summary); |
||||
DateTime timeMin = new DateTime(oldest); |
||||
List<Event> eventItems = service.events().list(calendar.getId()).setTimeMin(timeMin).execute() |
||||
.getItems(); |
||||
eventItems.forEach(item -> item.set("colorClass", colorClass)); |
||||
events.addAll(eventItems); |
||||
log.debug("Added {} Events for \"{}\"", eventItems.size(), summary); |
||||
|
||||
} catch (IOException e) { |
||||
log.error("Unable to fetch Events from {}", summary, e); |
||||
} |
||||
} |
||||
} |
||||
|
||||
static Event createDefaultEvent(String summary) { |
||||
Event ev = new Event().setGuestsCanInviteOthers(false).setGuestsCanModify(false) |
||||
.setGuestsCanSeeOtherGuests(true).setSummary(summary); |
||||
List<EventAttendee> attendees = new ArrayList<>(); |
||||
ev.setAttendees(attendees); |
||||
return ev; |
||||
} |
||||
|
||||
} |
||||
@ -1,80 +0,0 @@ |
||||
package de.kreth.clubhelperbackend.google.calendar; |
||||
|
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
import java.util.ArrayList; |
||||
import java.util.Enumeration; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Properties; |
||||
import java.util.StringTokenizer; |
||||
|
||||
public class CalendarResource { |
||||
|
||||
private Map<String, CalendarKonfig> configs; |
||||
|
||||
public CalendarResource() throws IOException { |
||||
configs = new HashMap<>(); |
||||
|
||||
InputStream resStream = getClass() |
||||
.getResourceAsStream("/calendars.properties"); |
||||
Properties prop = new Properties(); |
||||
prop.load(resStream); |
||||
|
||||
Enumeration<Object> keys = prop.keys(); |
||||
String className = getClass().getName(); |
||||
String packageName = className.substring(0, className.lastIndexOf('.')); |
||||
|
||||
while (keys.hasMoreElements()) { |
||||
String key = keys.nextElement().toString(); |
||||
String name = key.substring(packageName.length()); |
||||
String value = prop.getProperty(key); |
||||
|
||||
StringTokenizer tok = new StringTokenizer(name, "."); |
||||
name = tok.nextToken(); |
||||
String type = tok.nextToken(); |
||||
CalendarKonfig conf; |
||||
|
||||
if (configs.containsKey(name)) { |
||||
conf = configs.get(name); |
||||
} else { |
||||
conf = new CalendarKonfig(null, null); |
||||
configs.put(name, conf); |
||||
} |
||||
switch (type) { |
||||
case "name" : |
||||
conf.name = value; |
||||
break; |
||||
case "color" : |
||||
conf.color = value; |
||||
break; |
||||
default : |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
public List<CalendarKonfig> getConfigs() { |
||||
return new ArrayList<>(configs.values()); |
||||
} |
||||
|
||||
public static class CalendarKonfig { |
||||
private String name; |
||||
private String color; |
||||
|
||||
CalendarKonfig(String name, String color) { |
||||
super(); |
||||
this.name = name; |
||||
this.color = color; |
||||
} |
||||
|
||||
public String getName() { |
||||
return name; |
||||
} |
||||
|
||||
public String getColor() { |
||||
return color; |
||||
} |
||||
} |
||||
} |
||||
@ -1,98 +0,0 @@ |
||||
package de.kreth.vaadin.clubhelper.vaadinclubhelper.business; |
||||
|
||||
import java.io.IOException; |
||||
import java.security.GeneralSecurityException; |
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
import javax.validation.ConstraintViolationException; |
||||
|
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.dao.DataIntegrityViolationException; |
||||
import org.springframework.scheduling.annotation.Scheduled; |
||||
import org.springframework.stereotype.Component; |
||||
|
||||
import de.kreth.clubhelperbackend.google.calendar.CalendarAdapter; |
||||
import de.kreth.vaadin.clubhelper.vaadinclubhelper.dao.ClubEventDao; |
||||
import de.kreth.vaadin.clubhelper.vaadinclubhelper.data.ClubEvent; |
||||
|
||||
@Component |
||||
public class CalendarTaskRefresher { |
||||
|
||||
public static final String SKIP_EVENT_UPDATE = "skipEventUpdate"; |
||||
|
||||
private static final long RATE = 1000L * 60 * 10; |
||||
private final Logger log = LoggerFactory.getLogger(getClass()); |
||||
private final boolean skip = Boolean.parseBoolean(System.getProperty(SKIP_EVENT_UPDATE, "false")); |
||||
|
||||
@Autowired |
||||
ClubEventDao dao; |
||||
@Autowired |
||||
CalendarAdapter calendarAdapter; |
||||
|
||||
@Scheduled(fixedDelay = RATE) |
||||
public void synchronizeCalendarTasks() { |
||||
if (skip) { |
||||
return; |
||||
} |
||||
|
||||
List<ClubEvent> list = loadEventsFromGoogle(); |
||||
|
||||
for (ClubEvent e : list) { |
||||
if (dao.get(e.getId()) == null) { |
||||
try { |
||||
log.trace("try inserting {}", e); |
||||
dao.save(e); |
||||
} catch (ConstraintViolationException | DataIntegrityViolationException ex) { |
||||
log.warn("Insert failed, updating {}", e); |
||||
dao.update(e); |
||||
} |
||||
} else { |
||||
log.trace("try updating {}", e); |
||||
dao.update(e); |
||||
} |
||||
log.debug("successfully stored {}", e); |
||||
} |
||||
} |
||||
|
||||
public List<ClubEvent> loadEventsFromGoogle() { |
||||
|
||||
log.debug("Loading events from Google Calendar"); |
||||
|
||||
List<ClubEvent> list = new ArrayList<>(); |
||||
|
||||
try (FileExporter ex = FileExporter.builder(log).setFileName("google_events.json").setAppend(false).disable() |
||||
.build()) { |
||||
|
||||
String remoteHost = "localhost"; |
||||
|
||||
List<com.google.api.services.calendar.model.Event> events = calendarAdapter.getAllEvents(remoteHost); |
||||
|
||||
for (com.google.api.services.calendar.model.Event ev : events) { |
||||
ex.writeLine(ev.toPrettyString()); |
||||
|
||||
if ("cancelled".equals(ev.getStatus())) { |
||||
log.debug("Cancelled: {}", ev.getSummary()); |
||||
} else { |
||||
list.add(ClubEvent.parse(ev)); |
||||
} |
||||
} |
||||
|
||||
} catch (GeneralSecurityException | IOException | InterruptedException e) { |
||||
log.error("Error loading events from google.", e); |
||||
} catch (Exception e1) { |
||||
log.warn("Error closing " + FileExporter.class.getSimpleName(), e1); |
||||
} |
||||
return list; |
||||
} |
||||
|
||||
public void setDao(ClubEventDao dao) { |
||||
this.dao = dao; |
||||
} |
||||
|
||||
public void setCalendarAdapter(CalendarAdapter calendarAdapter) { |
||||
this.calendarAdapter = calendarAdapter; |
||||
} |
||||
} |
||||
@ -1,48 +0,0 @@ |
||||
package de.kreth.vaadin.clubhelper.vaadinclubhelper.business; |
||||
|
||||
import static org.mockito.ArgumentMatchers.anyString; |
||||
import static org.mockito.Mockito.verify; |
||||
import static org.mockito.Mockito.when; |
||||
|
||||
import java.io.IOException; |
||||
import java.util.Collections; |
||||
|
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.mockito.Mock; |
||||
import org.mockito.MockitoAnnotations; |
||||
|
||||
import de.kreth.clubhelperbackend.google.calendar.CalendarAdapter; |
||||
import de.kreth.vaadin.clubhelper.vaadinclubhelper.dao.ClubEventDao; |
||||
|
||||
class CalendarTaskRefresherTest { |
||||
|
||||
@Mock |
||||
private ClubEventDao dao; |
||||
@Mock |
||||
private CalendarAdapter calendarAdapter; |
||||
|
||||
@BeforeEach |
||||
void setUp() throws Exception { |
||||
MockitoAnnotations.initMocks(this); |
||||
when(calendarAdapter.getAllEvents(anyString())).thenReturn(Collections.emptyList()); |
||||
} |
||||
|
||||
@Test |
||||
void testSkip() throws IOException, InterruptedException { |
||||
System.setProperty(CalendarTaskRefresher.SKIP_EVENT_UPDATE, Boolean.FALSE.toString()); |
||||
CalendarTaskRefresher r = new CalendarTaskRefresher(); |
||||
r.setDao(dao); |
||||
r.setCalendarAdapter(calendarAdapter); |
||||
r.synchronizeCalendarTasks(); |
||||
verify(calendarAdapter).getAllEvents(anyString()); |
||||
System.setProperty(CalendarTaskRefresher.SKIP_EVENT_UPDATE, Boolean.TRUE.toString()); |
||||
|
||||
r = new CalendarTaskRefresher(); |
||||
r.setDao(dao); |
||||
r.setCalendarAdapter(calendarAdapter); |
||||
r.synchronizeCalendarTasks(); |
||||
verify(calendarAdapter).getAllEvents(anyString()); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue