@ -11,10 +11,8 @@ import com.sun.net.httpserver.HttpExchange;
@@ -11,10 +11,8 @@ import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.cal.api.Appointment ;
import de.srsoftware.cal.api.Link ;
import de.srsoftware.cal.db.Database ;
import de.srsoftware.tools.* ;
import de.srsoftware.tools.Error ;
import de.srsoftware.tools.PathHandler ;
import de.srsoftware.tools.Payload ;
import de.srsoftware.tools.Result ;
import java.io.IOException ;
import java.sql.SQLException ;
import java.time.LocalDate ;
@ -22,6 +20,7 @@ import java.time.LocalDateTime;
@@ -22,6 +20,7 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter ;
import java.util.List ;
import java.util.Map ;
import java.util.Optional ;
import org.json.JSONObject ;
public class ApiHandler extends PathHandler {
@ -44,12 +43,42 @@ public class ApiHandler extends PathHandler {
@@ -44,12 +43,42 @@ public class ApiHandler extends PathHandler {
case "/tags" - > listTags ( ex , params ) ;
default - > PathHandler . notFound ( ex ) ;
} ;
}
@Override
public boolean doPost ( String path , HttpExchange ex ) throws IOException {
public boolean doDelete ( String path , HttpExchange ex ) throws IOException {
var params = queryParam ( ex ) ;
return switch ( path ) {
case "/event" - > delete ( ex , params ) ;
default - > PathHandler . notFound ( ex ) ;
} ;
}
private boolean delete ( HttpExchange ex , Map < String , String > params ) throws IOException {
var aid = params . get ( AID ) ;
if ( aid = = null ) return sendContent ( ex , Error . of ( "Missing appointment id" ) ) ;
long id = 0 ;
try {
id = Long . parseLong ( aid ) ;
} catch ( Exception e ) {
return sendContent ( ex , Error . format ( "%s is not a valid id!" , aid ) ) ;
}
var json = json ( ex ) ;
var title = json . has ( TITLE ) ? nullIfEmpty ( json . getString ( TITLE ) ) :
null ;
if ( title = = null ) return sendContent ( ex , Error . of ( "Missing appointment title" ) ) ;
var res = db . loadEvent ( id ) ;
if ( res . optional ( ) . isEmpty ( ) ) return sendContent ( ex , res ) ;
var event = res . optional ( ) . get ( ) ;
if ( ! title . equals ( event . title ( ) ) ) return sendContent ( ex , Error . of ( "Title mismatch!" ) ) ;
var del = db . removeAppointment ( id ) ;
if ( del . optional ( ) . isEmpty ( ) ) return sendContent ( ex , del ) ;
return sendContent ( ex , Map . of ( "deleted" , del . optional ( ) . get ( ) ) ) ;
}
@Override
public boolean doPost ( String path , HttpExchange ex ) throws IOException {
return switch ( path ) {
case "/event/edit" - > editEvent ( ex ) ;
default - > PathHandler . notFound ( ex ) ;
} ;
@ -58,129 +87,135 @@ public class ApiHandler extends PathHandler {
@@ -58,129 +87,135 @@ public class ApiHandler extends PathHandler {
// spotless:off
private boolean editEvent ( HttpExchange ex ) throws IOException {
var json = json ( ex ) ;
var location = json . has ( LOCATION ) ? json . getString ( LOCATION ) : null ;
var start = json . has ( START ) ? LocalDateTime . parse ( json . getString ( START ) ) : null ;
if ( allSet ( location , start ) ) {
var existingAppointment = db . loadEvent ( location , start ) . optional ( ) ;
if ( existingAppointment . isPresent ( ) ) {
var event = existingAppointment . get ( ) ;
json . put ( AID , event . id ( ) ) ;
return update ( ex , toEvent ( json ) ) ;
Optional < Appointment > existingAppointment = Optional . empty ( ) ;
Long aid = json . has ( AID ) ? json . getLong ( AID ) : null ;
if ( aid ! = null ) { // load appointment by aid
existingAppointment = db . loadEvent ( aid ) . optional ( ) ;
} else { // try to load appointment by location @ time
var location = json . has ( LOCATION ) ? json . getString ( LOCATION ) : null ;
var start = json . has ( START ) ? LocalDateTime . parse ( json . getString ( START ) ) : null ;
if ( allSet ( location , start ) ) {
existingAppointment = db . loadEvent ( location , start ) . optional ( ) ;
}
}
if ( existingAppointment . isPresent ( ) ) {
var event = existingAppointment . get ( ) ;
json . put ( AID , event . id ( ) ) ;
return update ( ex , toEvent ( json ) ) ;
}
return createEvent ( ex , json ) ;
}
// spotless:on
private Result < BaseAppointment > toEvent ( JSONObject json ) {
var description = json . has ( DESCRIPTION ) ? nullIfEmpty ( json . getString ( DESCRIPTION ) ) : null ;
var title = json . has ( TITLE ) ? nullIfEmpty ( json . getString ( TITLE ) ) : null ;
if ( title = = null ) return Error . of ( "title missing" ) ;
var start = json . has ( START ) ? nullIfEmpty ( json . getString ( START ) ) : null ;
if ( start = = null ) return Error . of ( "start missing" ) ;
var startDate = nullable ( start ) . map ( dt - > LocalDateTime . parse ( dt , ISO_DATE_TIME ) ) . orElse ( null ) ;
var end = json . has ( END ) ? nullIfEmpty ( json . getString ( END ) ) : null ;
var endDate = nullable ( end ) . map ( dt - > LocalDateTime . parse ( dt , ISO_DATE_TIME ) ) . orElse ( null ) ;
var location = json . has ( LOCATION ) ? json . getString ( LOCATION ) : null ;
if ( location = = null ) return Error . of ( "location missing" ) ;
var aid = json . has ( AID ) ? json . getLong ( AID ) : 0 ;
var event = new BaseAppointment ( aid , title , description , startDate , endDate , location ) ;
if ( json . has ( ATTACHMENTS ) ) {
json . getJSONArray ( ATTACHMENTS ) . forEach ( att - > {
Payload //
. of ( att . toString ( ) )
. map ( BaseImporter : : url )
. map ( BaseImporter : : toAttachment )
. optional ( )
. ifPresent ( event : : add ) ;
} ) ;
}
if ( json . has ( LINKS ) ) {
json . getJSONArray ( LINKS ) . forEach ( o - > {
if ( o instanceof JSONObject j ) toLink ( j ) . optional ( ) . ifPresent ( event : : addLinks ) ;
} ) ;
private Result < BaseAppointment > toEvent ( JSONObject json ) {
var description = json . has ( DESCRIPTION ) ? nullIfEmpty ( json . getString ( DESCRIPTION ) ) : null ;
var title = json . has ( TITLE ) ? nullIfEmpty ( json . getString ( TITLE ) ) : null ;
if ( title = = null ) return Error . of ( "title missing" ) ;
var start = json . has ( START ) ? nullIfEmpty ( json . getString ( START ) ) : null ;
if ( start = = null ) return Error . of ( "start missing" ) ;
var startDate = nullable ( start ) . map ( dt - > LocalDateTime . parse ( dt , ISO_DATE_TIME ) ) . orElse ( null ) ;
var end = json . has ( END ) ? nullIfEmpty ( json . getString ( END ) ) : null ;
var endDate = nullable ( end ) . map ( dt - > LocalDateTime . parse ( dt , ISO_DATE_TIME ) ) . orElse ( null ) ;
var location = json . has ( LOCATION ) ? json . getString ( LOCATION ) : null ;
if ( location = = null ) return Error . of ( "location missing" ) ;
var aid = json . has ( AID ) ? json . getLong ( AID ) : 0 ;
var event = new BaseAppointment ( aid , title , description , startDate , endDate , location ) ;
if ( json . has ( ATTACHMENTS ) ) {
json . getJSONArray ( ATTACHMENTS ) . forEach ( att - > {
Payload //
. of ( att . toString ( ) )
. map ( BaseImporter : : url )
. map ( BaseImporter : : toAttachment )
. optional ( )
. ifPresent ( event : : add ) ;
} ) ;
}
if ( json . has ( LINKS ) ) {
json . getJSONArray ( LINKS ) . forEach ( o - > {
if ( o instanceof JSONObject j ) toLink ( j ) . optional ( ) . ifPresent ( event : : addLinks ) ;
} ) ;
}
if ( json . has ( TAGS ) ) json . getJSONArray ( TAGS ) . forEach ( o - > event . tags ( o . toString ( ) ) ) ;
return Payload . of ( event ) ;
}
if ( json . has ( TAGS ) ) json . getJSONArray ( TAGS ) . forEach ( o - > event . tags ( o . toString ( ) ) ) ;
return Payload . of ( event ) ;
}
private boolean createEvent ( HttpExchange ex , JSONObject json ) throws IOException {
var eventRes = toEvent ( json ) ;
if ( eventRes . optional ( ) . isPresent ( ) ) {
return sendContent ( ex , db . add ( eventRes . optional ( ) . get ( ) ) . map ( ApiHandler : : toJson ) ) ;
private boolean createEvent ( HttpExchange ex , JSONObject json ) throws IOException {
var eventRes = toEvent ( json ) ;
if ( eventRes . optional ( ) . isPresent ( ) ) {
return sendContent ( ex , db . add ( eventRes . optional ( ) . get ( ) ) . map ( ApiHandler : : toJson ) ) ;
}
return sendContent ( ex , eventRes ) ;
}
return sendContent ( ex , eventRes ) ;
}
protected static Result < Link > toLink ( JSONObject json ) {
try {
var description = json . getString ( DESCRIPTION ) ;
return Payload . of ( json . getString ( URL ) ) . map ( BaseImporter : : url ) . map ( url - > BaseImporter . link ( url , description ) ) ;
protected static Result < Link > toLink ( JSONObject json ) {
try {
var description = json . getString ( DESCRIPTION ) ;
return Payload . of ( json . getString ( URL ) ) . map ( BaseImporter : : url ) . map ( url - > BaseImporter . link ( url , description ) ) ;
} catch ( Exception e ) {
return Error . of ( "Failed to create link from %s" . formatted ( json ) , e ) ;
} catch ( Exception e ) {
return Error . of ( "Failed to create link from %s" . formatted ( json ) , e ) ;
}
}
}
private boolean update ( HttpExchange ex , Result < BaseAppointment > event ) throws IOException {
if ( event . optional ( ) . isPresent ( ) ) return sendContent ( ex , db . update ( event . optional ( ) . get ( ) ) . map ( ApiHandler : : toJson ) ) ;
return sendContent ( ex , event ) ;
}
private boolean listTags ( HttpExchange ex , Map < String , String > params ) throws IOException {
var infix = params . get ( "infix" ) ;
if ( infix = = null ) return sendContent ( ex , Error . of ( "No infix set in method call parameters" ) ) ;
var res = db . findTags ( infix ) . map ( ApiHandler : : sortTags ) ;
return sendContent ( ex , res ) ;
}
private boolean update ( HttpExchange ex , Result < BaseAppointment > event ) throws IOException {
if ( event . optional ( ) . isPresent ( ) ) return sendContent ( ex , db . update ( event . optional ( ) . get ( ) ) . map ( ApiHandler : : toJson ) ) ;
return sendContent ( ex , event ) ;
}
private static Result < List < String > > sortTags ( Result < List < String > > listResult ) {
if ( listResult . optional ( ) . isEmpty ( ) ) return listResult ;
List < String > list = listResult . optional ( ) . get ( ) ;
while ( list . size ( ) > 15 ) {
int longest = list . stream ( ) . map ( String : : length ) . reduce ( 0 , Integer : : max ) ;
var subset = list . stream ( ) . filter ( s - > s . length ( ) < longest ) . toList ( ) ;
if ( subset . size ( ) < 3 ) return Payload . of ( list ) ;
list = subset ;
private boolean listTags ( HttpExchange ex , Map < String , String > params ) throws IOException {
var infix = params . get ( "infix" ) ;
if ( infix = = null ) return sendContent ( ex , Error . of ( "No infix set in method call parameters" ) ) ;
var res = db . findTags ( infix ) . map ( ApiHandler : : sortTags ) ;
return sendContent ( ex , res ) ;
}
return Payload . of ( list ) ;
}
private boolean listEvents ( HttpExchange ex , Map < String , String > params ) throws IOException {
var start = nullable ( params . get ( "start" ) ) . map ( ApiHandler : : toLocalDateTime ) . orElse ( null ) ;
var end = nullable ( params . get ( "end" ) ) . map ( ApiHandler : : toLocalDateTime ) . orElse ( null ) ;
try {
return PathHandler . sendContent ( ex , db . list ( start , end ) . stream ( ) . map ( Appointment : : json ) . toList ( ) ) ;
} catch ( SQLException e ) {
LOG . log ( WARNING , "Failed to fetch events (start = {0}, end = {1}!" , start , end , e ) ;
private static Result < List < String > > sortTags ( Result < List < String > > listResult ) {
if ( listResult . optional ( ) . isEmpty ( ) ) return listResult ;
List < String > list = listResult . optional ( ) . get ( ) ;
while ( list . size ( ) > 15 ) {
int longest = list . stream ( ) . map ( String : : length ) . reduce ( 0 , Integer : : max ) ;
var subset = list . stream ( ) . filter ( s - > s . length ( ) < longest ) . toList ( ) ;
if ( subset . size ( ) < 3 ) return Payload . of ( list ) ;
list = subset ;
}
return Payload . of ( list ) ;
}
return PathHandler . notFound ( ex ) ;
}
private boolean loadEvent ( HttpExchange ex , Map < String , String > params ) throws IOException {
var id = params . get ( "id" ) ;
if ( id ! = null ) try {
return PathHandler . sendContent ( ex , db . loadEvent ( Long . parseLong ( id ) ) . map ( ApiHandler : : toJson ) ) ;
} catch ( NumberFormatException | IOException nfe ) {
return PathHandler . sendContent ( ex , Error . format ( "%s is not a numeric event id!" , id ) ) ;
private boolean listEvents ( HttpExchange ex , Map < String , String > params ) throws IOException {
var start = nullable ( params . get ( "start" ) ) . map ( ApiHandler : : toLocalDateTime ) . orElse ( null ) ;
var end = nullable ( params . get ( "end" ) ) . map ( ApiHandler : : toLocalDateTime ) . orElse ( null ) ;
try {
return PathHandler . sendContent ( ex , db . list ( start , end ) . stream ( ) . map ( Appointment : : json ) . toList ( ) ) ;
} catch ( SQLException e ) {
LOG . log ( WARNING , "Failed to fetch events (start = {0}, end = {1}!" , start , end , e ) ;
}
return PathHandler . sendContent ( ex , Error . of ( "ID missing" ) ) ;
}
return PathHandler . notFound ( ex ) ;
}
private static Result < JSONObject > toJson ( Result < Appointment > res ) {
var opt = res . optional ( ) ;
if ( opt . isEmpty ( ) ) return Result . transform ( res ) ;
return Payload . of ( opt . get ( ) . json ( ) ) ;
}
private boolean loadEvent ( HttpExchange ex , Map < String , String > params ) throws IOException {
var id = params . get ( "id" ) ;
if ( id ! = null ) try {
return PathHandler . sendContent ( ex , db . loadEvent ( Long . parseLong ( id ) ) . map ( ApiHandler : : toJson ) ) ;
} catch ( NumberFormatException | IOException nfe ) {
return PathHandler . sendContent ( ex , Error . format ( "%s is not a numeric event id!" , id ) ) ;
}
return PathHandler . sendContent ( ex , Error . of ( "ID missing" ) ) ;
}
private static Result < JSONObject > toJson ( Result < Appointment > res ) {
var opt = res . optional ( ) ;
if ( opt . isEmpty ( ) ) return Result . transform ( res ) ;
return Payload . of ( opt . get ( ) . json ( ) ) ;
}
private static LocalDateTime toLocalDateTime ( String dateString ) {
try {
return LocalDate . parse ( dateString + "-01" , DateTimeFormatter . ISO_LOCAL_DATE ) . atTime ( 0 , 0 ) ;
} catch ( Exception e ) {
return null ;
private static LocalDateTime toLocalDateTime ( String dateString ) {
try {
return LocalDate . parse ( dateString + "-01" , DateTimeFormatter . ISO_LOCAL_DATE ) . atTime ( 0 , 0 ) ;
} catch ( Exception e ) {
return null ;
}
}
}
}