2020-07-03 13:05:23 +02:00
package nouritsu ;
import fi.iki.elonen.NanoHTTPD ;
import fi.iki.elonen.NanoHTTPD.Response.Status ;
2020-07-04 00:38:18 +02:00
import io.vavr.Tuple ;
2020-07-03 13:05:23 +02:00
import io.vavr.Tuple2 ;
import io.vavr.collection.HashMap ;
2020-07-05 00:20:47 +02:00
import io.vavr.collection.HashSet ;
2020-07-03 13:05:23 +02:00
import io.vavr.collection.Map ;
import io.vavr.control.Either ;
import io.vavr.control.Try ;
2020-07-04 00:38:18 +02:00
import nouritsu.types.Resp ;
import nouritsu.types.ShoppingItem ;
import nouritsu.types.Store ;
2020-07-03 13:05:23 +02:00
import java.io.IOException ;
2020-07-04 00:38:18 +02:00
import java.util.function.BiFunction ;
2020-07-03 13:05:23 +02:00
import java.util.function.Function ;
public class Nouritsu extends NanoHTTPD {
private final Dao dao ;
public Nouritsu ( int port , Dao dao ) throws IOException {
super ( port ) ;
start ( 10_000 , false ) ;
this . dao = dao ;
}
@Override
public Response serve ( IHTTPSession session ) {
return Try . of ( ( ) - > processRequest ( session , dao ) )
. fold (
2020-07-04 00:38:18 +02:00
// This won’t be exposed to the outside world, so returning the error message is fine
2020-07-05 00:20:47 +02:00
err - > {
err . printStackTrace ( ) ;
return newFixedLengthResponse ( Status . INTERNAL_ERROR , MIME_PLAINTEXT , err . getMessage ( ) ) ;
} ,
2020-07-03 13:05:23 +02:00
response - > newFixedLengthResponse ( response . status ( ) , MIME_PLAINTEXT , response . text ( ) )
) ;
}
private static final String HELP = """
Valid endpoints are ( not case sensitive ) :
/ addStore / ? name = storeName & sections = first , second , third
2020-07-05 00:20:47 +02:00
/ addItem / ? name = itemName & section = itemSection
2020-07-04 00:38:18 +02:00
/ addToList / ? user = userId & item = itemName
2020-07-05 00:20:47 +02:00
/ getList / ? user = userId
/ getListForStore / ? user = userId & store = StoreName
2020-07-03 13:05:23 +02:00
/ clearList / ? user = userId
" " " ;
2020-07-04 00:38:18 +02:00
private static Resp processRequest ( IHTTPSession session , Dao dao ) {
// Type inference can’t handle this :feelsBadMan:
2020-07-05 00:38:43 +02:00
final BiFunction < Map < String , String > , Dao , Resp > handler =
2020-07-04 00:38:18 +02:00
switch ( session . getUri ( ) . replaceAll ( " / " , " " ) . toLowerCase ( ) ) {
case " addstore " - > Nouritsu : : addStore ;
case " additem " - > Nouritsu : : addItem ;
case " addtolist " - > Nouritsu : : addToList ;
case " getlist " - > Nouritsu : : getList ;
2020-07-05 00:20:47 +02:00
case " getlistforstore " - > Nouritsu : : getListOrdered ;
2020-07-04 00:38:18 +02:00
case " clearlist " - > Nouritsu : : clearList ;
default - > ( _1 , _2 ) - > Resp . NOT_FOUND . apply ( HELP ) ;
} ;
return handler . apply ( extractParams ( session ) , dao ) ;
2020-07-03 13:05:23 +02:00
}
2020-07-04 00:38:18 +02:00
// Repack the parameters into a vavr Map<Key, Head(Value)>.
// Each parameter should only occur once per request.
private static Map < String , String > extractParams ( IHTTPSession session ) {
2020-07-03 13:05:23 +02:00
return session . getParameters ( )
. entrySet ( )
. stream ( )
2020-07-04 00:38:18 +02:00
. map ( Tuple : : fromEntry )
. collect ( HashMap . collector ( ) )
// guaranteed to be non-empty
. mapValues ( list - > list . get ( 0 ) ) ;
2020-07-03 13:05:23 +02:00
}
2020-07-04 00:38:18 +02:00
private static Resp clearList ( Map < String , String > params , Dao dao ) {
2020-07-03 13:05:23 +02:00
return getFromParams ( params , " user " )
2020-07-04 00:38:18 +02:00
. map ( dao : : clearList )
. fold ( Function . identity ( ) , Resp . CREATED ) ;
2020-07-03 13:05:23 +02:00
}
2020-07-04 00:38:18 +02:00
private static Resp getList ( Map < String , String > params , Dao dao ) {
2020-07-03 13:05:23 +02:00
return getFromParams ( params , " user " )
. flatMap ( dao : : getList )
2020-07-04 00:38:18 +02:00
. map ( shoppingItems - > shoppingItems . mkString ( " , " ) )
. fold ( Function . identity ( ) , Resp . OK ) ;
2020-07-03 13:05:23 +02:00
}
2020-07-05 00:20:47 +02:00
private static Resp getListOrdered ( Map < String , String > params , Dao dao ) {
return getAsTuple ( params , " user " , " store " )
. flatMap ( t - > t . apply ( ( userId , storeName ) - >
zip ( dao . getList ( userId ) , dao . getStore ( storeName ) )
) )
. map ( t - > t . apply ( Nouritsu : : sortBySection ) )
. fold ( Function . identity ( ) , Resp . OK ) ;
}
2020-07-05 00:27:39 +02:00
// Return a Tuple2 containing the values of two eithers if both were Either.Right
private static < L , R1 , R2 > Either < L , Tuple2 < R1 , R2 > > zip ( Either < L , R1 > first , Either < L , R2 > second ) {
return first . flatMap ( f - > second . map ( s - > Tuple . of ( f , s ) ) ) ;
}
2020-07-05 00:38:43 +02:00
// This kind of breaks the style,
// but I can’t come up with a more elegant solution,
// so lots of locals it is.
2020-07-05 00:20:47 +02:00
private static String sortBySection ( HashSet < ShoppingItem > items , Store store ) {
2020-07-05 00:38:43 +02:00
final var itemsBySection = items . groupBy ( ShoppingItem : : section ) ;
final var sorted = store . sections ( ) . flatMap ( s - > itemsBySection . get ( s ) . getOrElse ( HashSet . empty ( ) ) ) ;
final var rest = items . removeAll ( sorted ) ;
final var formattedItems = sorted . map ( ShoppingItem : : name ) . mkString ( " , " ) ;
2020-07-05 00:20:47 +02:00
if ( rest . isEmpty ( ) ) {
2020-07-05 00:27:39 +02:00
return formattedItems ;
2020-07-05 00:20:47 +02:00
}
2020-07-05 00:27:39 +02:00
return formattedItems + rest . mkString ( " \ nItems not available or not classified in this store: " , " , " , " " ) ;
2020-07-05 00:20:47 +02:00
}
2020-07-04 00:38:18 +02:00
private static Resp addToList ( Map < String , String > params , Dao dao ) {
return getAsTuple ( params , " user " , " item " )
// This is where Kotlin-like destructuring would be really nice to have:
// flatMap(id, item -> ...)
2020-07-04 01:00:18 +02:00
. flatMap ( t - > t . apply ( dao : : addToList ) )
2020-07-04 00:38:18 +02:00
. fold ( Function . identity ( ) , Resp . CREATED ) ;
2020-07-03 13:05:23 +02:00
}
2020-07-04 00:38:18 +02:00
private static Resp addItem ( Map < String , String > params , Dao dao ) {
2020-07-05 00:20:47 +02:00
return getAsTuple ( params , " name " , " section " )
2020-07-04 01:00:18 +02:00
. flatMap ( t - > t . apply ( ShoppingItem : : tryParse ) )
2020-07-04 00:38:18 +02:00
. map ( dao : : addItem )
. fold ( Function . identity ( ) , Resp . CREATED ) ;
2020-07-03 13:05:23 +02:00
}
2020-07-04 00:38:18 +02:00
private static Resp addStore ( Map < String , String > params , Dao dao ) {
2020-07-05 00:20:47 +02:00
return getAsTuple ( params , " name " , " sections " )
2020-07-04 01:00:18 +02:00
. flatMap ( t - > t . apply ( Store : : tryParse ) )
2020-07-05 00:20:47 +02:00
. map ( dao : : addStore )
2020-07-04 00:38:18 +02:00
. fold ( Function . identity ( ) , Resp . CREATED ) ;
2020-07-03 13:05:23 +02:00
}
2020-07-04 00:38:18 +02:00
private static Either < Resp , String > getFromParams ( Map < String , String > params , String key ) {
return params . get ( key ) . toEither ( Resp . NOT_FOUND . apply ( " Parameter “%s” not found " . formatted ( key ) ) ) ;
}
// Get k1 and k2 as a tuple in Either.Right or Either.Left if k1 or k2 is/are not in the map.
private static Either < Resp , Tuple2 < String , String > > getAsTuple ( Map < String , String > params , String k1 , String k2 ) {
2020-07-05 21:33:51 +02:00
return zip ( getFromParams ( params , k1 ) , getFromParams ( params , k2 ) ) ;
2020-07-03 13:05:23 +02:00
}
public static void main ( String [ ] args ) {
try {
new Nouritsu ( 14523 , new Dao ( ) ) ;
} catch ( IOException e ) {
e . printStackTrace ( ) ;
}
}
}