/**
* Copyright 2014 University of Chicago
*
* Licensed 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.
*
* Author: Daniel Yu <danielyu@uchicago.edu>
*/
package edu.uchicago.duo.web;
import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import edu.uchicago.duo.domain.DuoAllIntegrationKeys;
import edu.uchicago.duo.domain.DuoPersonObj;
import edu.uchicago.duo.service.DuoObjInterface;
import edu.uchicago.duo.validator.DeviceExistDuoValidator;
import static edu.uchicago.duo.web.DuoEnrollController.sortByValue;
import java.io.UnsupportedEncodingException;
import java.security.Principal;
import java.sql.Date;
import java.text.Collator;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.Locale;
import java.util.TreeMap;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.validation.Valid;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.Logger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.SmartValidator;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;
@Controller
@RequestMapping("/secure/enrollment")
@SessionAttributes("DuoPerson")
public class DuoEnrollController {
//get log4j handler
protected final Log logger = LogFactory.getLog(getClass());
///
@Autowired
private DuoObjInterface duoPhoneService;
///
@Autowired
private DuoObjInterface duoUsrService;
///
@Autowired
private DuoObjInterface duoTokenService;
///
@Autowired
private DeviceExistDuoValidator deviceExistDuoValidator;
///
@Autowired
SmartValidator validator;
/**
* **********************************************************
*
* Private Methods Below
*
***********************************************************
*/
private String getIPForLog(HttpServletRequest request) {
String sourceIPAddr = request.getRemoteAddr();
if (sourceIPAddr == null || sourceIPAddr.startsWith("127.")) {
sourceIPAddr = request.getHeader("x-forwarded-for");
}
sourceIPAddr = "[" + sourceIPAddr + "]";
return sourceIPAddr;
}
/**
* **********************************************************
*
* Spring Controller Methods Below
*
***********************************************************
*/
@InitBinder
public void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true));
}
/*
* Below initalize the model and return the view, Setup NEW session Attribute for "DuoPerson",
* according to Spring Docs, it is recommended to NOT share session attributes among different controller.
* It makes sense to me in that when user bookmarked this specific entry point, sharing a common session attribute may not
* get all the value the controller needed.
* Note about session attribute:
* @ModelAttribute("DuoPerson") DuoPersonObj duoperson <Look for existing session attribute>
* @ModelAttribute DuoPersonObj duoperson <Initalize a New session attribute>
*/
@RequestMapping(method = RequestMethod.GET)
public String initForm(HttpServletRequest request, Principal principal, ModelMap model, @ModelAttribute DuoPersonObj duoperson,
HttpSession session, SessionStatus status) throws UnsupportedEncodingException, JSONException, Exception {
//Below getting SSO Attributes for Shibboleth Support(UChicago)
// duoperson.setUsername(principal.getName());
// duoperson.setFullName(request.getHeader("givenName")+ " " + request.getHeader("sn"));
// duoperson.setEmail(request.getHeader("mail"));
// duoperson.setChicagoID(request.getHeader("chicagoID"));
//Below setting Static Attributes for Local Testing
duoperson.setUsername("DuoTestUser");
duoperson.setFullName("DUO Testuser");
duoperson.setEmail("testuser@duotest.com");
logger.info("2FA Info - " + getIPForLog(request) + " - " + "Username:" + duoperson.getUsername() + "|SID:" + request.getSession().getId());
//Setup Default Selection Values for the wizard form
duoperson.setChoosenDevice("mobile");
duoperson.setDeviceOS("apple ios");
duoperson.setCountryDialCode("+1");
//Initalize Model with some variables and push that into SessionAttribute
model.addAttribute("DuoPerson", duoperson);
//return form view
return processPage(2, duoperson, request, principal, null, session, status, model);
}
@RequestMapping(method = RequestMethod.POST, params = "reset")
public String resetform(ModelMap model, @ModelAttribute("DuoPerson") DuoPersonObj duoperson, HttpSession session, SessionStatus status) {
status.setComplete();
return "redirect:/secure";
}
@RequestMapping(method = RequestMethod.POST, params = "back")
public String goBack(@RequestParam("_backpage") final int backPage, ModelMap model, @ModelAttribute("DuoPerson") DuoPersonObj duoperson, HttpSession session, SessionStatus status) {
if (backPage == 31) {
return "DuoEnrollTablet";
}
return "DuoEnrollStep" + backPage;
}
@RequestMapping(method = RequestMethod.POST, params = "enrollsteps")
public String processPage(@RequestParam("_page") final int nextPage,
@ModelAttribute("DuoPerson") DuoPersonObj duoperson, HttpServletRequest request, Principal principal,
BindingResult result, HttpSession session, SessionStatus status, ModelMap model) throws UnsupportedEncodingException, JSONException, Exception {
//Redirect for Enroll Another Device
if (nextPage == 0) {
status.setComplete();
return "redirect:/secure/enrollment";
}
if (nextPage == 2) {
String userId = duoUsrService.getObjByParam(duoperson.getUsername(), null, "userId");
duoperson.setUser_id(userId);
if (userId != null) {
//Session Attribute "Duo User ID" - UPDATED (Depends)
session.setAttribute("duoUserId", userId);
model.put("existingUser", true);
}
}
//Redirect Depends on the type of Device that is being enroll
if (nextPage == 3) {
switch (duoperson.getChoosenDevice()) {
case "tablet":
duoperson.setDeviceOS(null);
return "DuoEnrollTablet";
case "token":
return "DuoEnrollToken";
}
}
if (nextPage == 31) {
validator.validate(duoperson, result, DuoPersonObj.TabletInfoValidation.class);
if (result.hasErrors()) {
return "DuoEnrollTablet";
}
return "DuoEnrollStep5";
}
if (nextPage == 32) {
validator.validate(duoperson, result, DuoPersonObj.TokenInfoValidation.class);
if (result.hasErrors()) {
return "DuoEnrollToken";
}
return processEnroll(request, duoperson, result, session, status, model);
}
//Validation on Submission of Phone Number, to make sure Phone Number has not been registered or belong to someone else
if (nextPage == 4) {
validator.validate(duoperson, result, DuoPersonObj.PhoneNumberValidation.class);
if (result.hasErrors()) {
return "DuoEnrollStep3";
}
deviceExistDuoValidator.validate(duoperson, result);
if (result.hasErrors()) {
return "DuoEnrollStep3";
}
if (duoperson.getChoosenDevice().equals("landline")) {
return "DuoPhoneVerify";
}
}
if (nextPage == 5) {
if (duoperson.getDeviceOS().equals("unknown")) {
return "DuoPhoneVerify";
}
}
//Check on Activation Status on DUO Mobile App, don't let user move on until confirm the device has been activated
if (nextPage == 6) {
String activeStatus;
activeStatus = duoPhoneService.getObjStatusById(duoperson.getPhone_id());
if (activeStatus.equals("false")) {
if (duoperson.getQRcode() == null) {
String qrCode = duoPhoneService.objActionById(duoperson.getPhone_id(), "qrCode");
duoperson.setQRcode(qrCode);
}
logger.error("2FA Error - " + getIPForLog(request) + " - " + duoperson.getUsername() + "|DeviceID: " + duoperson.getPhone_id() + " NOT ACTIVATED");
model.put("deviceNotActive", true);
return "DuoActivationQR";
}
model.put("deviceActive", "Yes");
logger.info("2FA Info - " + getIPForLog(request) + " - " + duoperson.getUsername() + " Successfully Registered: PhoneNumber:" + duoperson.getPhonenumber()
+ " Tablet Name:" + duoperson.getTabletName());
return "DuoEnrollSuccess";
}
//Traverse Multipage Form, DuoEnrollStep*.jsp
return "DuoEnrollStep" + nextPage;
}
@RequestMapping(value = "/phoneverify.json/{action}", method = RequestMethod.GET)
@ResponseBody
public String callToVerify(@ModelAttribute("DuoPerson") DuoPersonObj duoperson, HttpSession session, @PathVariable String action) {
String callInfo = null;
String callState = null;
Map<String, Object> verifyInfo = new HashMap<>();
switch (action) {
case "call":
verifyInfo = duoPhoneService.verifyObj(duoperson.getCompletePhonenumber(), duoperson.getLandLineExtension(), action);
duoperson.setPhoneVerifyPin(verifyInfo.get("pin").toString());
duoperson.setPhoneVerifyTxid(verifyInfo.get("txid").toString());
return "CALLING";
case "status":
verifyInfo = duoPhoneService.verifyObj(duoperson.getPhoneVerifyTxid(), null, action);
callInfo = verifyInfo.get("info").toString();
callState = verifyInfo.get("state").toString();
break;
}
return callInfo;
}
@RequestMapping(value = "/phoneverify.json/verify/{inputpin}", method = RequestMethod.GET)
@ResponseBody
public String verifyPin(@ModelAttribute("DuoPerson") DuoPersonObj duoperson, HttpSession session, @PathVariable String inputpin, HttpServletRequest request) {
String correctPin = null;
correctPin = duoperson.getPhoneVerifyPin();
if (inputpin.equals(correctPin)) {
duoperson.setPhoneOwnerVerified(true);
logger.info("2FA Info - " + getIPForLog(request) + " - " + "Username:" + duoperson.getUsername() + " VERIFIED PhoneNumber:" + duoperson.getPhonenumber() + " Extension:" + duoperson.getLandLineExtension());
return "VERIFIED";
} else {
duoperson.setPhoneOwnerVerified(false);
return "INCORRECT";
}
}
@RequestMapping("/activationstatus.json")
@ResponseBody
public String reportStatus(@ModelAttribute("DuoPerson") DuoPersonObj duoperson, HttpSession session) {
return duoPhoneService.getObjStatusById(duoperson.getPhone_id());
}
@RequestMapping(method = RequestMethod.POST, params = "sendsms")
public String duoSendSMS(
@ModelAttribute("DuoPerson") DuoPersonObj duoperson, HttpServletRequest request,
BindingResult result, SessionStatus status, ModelMap model) throws UnsupportedEncodingException, Exception {
duoPhoneService.objActionById(duoperson.getPhone_id(), "activationSMS");
logger.info("2FA Info - " + getIPForLog(request) + " - " + duoperson.getUsername() + " is sending ACTIVATION SMS to " + duoperson.getPhonenumber());
duoperson.setQRcode(null);
return "DuoActivationQR";
}
@RequestMapping(method = RequestMethod.POST, params = "genQRcode")
public String genQRCode(
@ModelAttribute("DuoPerson") DuoPersonObj duoperson, HttpServletRequest request,
BindingResult result, SessionStatus status, ModelMap model) throws UnsupportedEncodingException, Exception {
String qrCode = duoPhoneService.objActionById(duoperson.getPhone_id(), "qrCode");
duoperson.setQRcode(qrCode);
logger.debug("2FA Debug - " + getIPForLog(request) + " - " + duoperson.getUsername() + "|DeviceID: " + duoperson.getPhone_id() + " QR code regenrated");
return "DuoActivationQR";
}
@RequestMapping(value = "/2FAoptin")
public String twoFactorOptIn(@RequestParam(value = "_action", required = false, defaultValue = "landed") final String action,
@ModelAttribute("DuoPerson") DuoPersonObj duoperson, HttpSession session, HttpServletRequest request) {
String result = null;
switch (action) {
case "landed":
duoperson.setOptInStatus(true);
break;
case "optin":
if (duoperson.isOptInStatus()) {
result = duoUsrService.objActionById(duoperson.getChicagoID(), "AddUserToDuoForce");
if (result.equals("Y")) {
duoperson.setOptInStatus(true);
logger.info("2FA Info - " + getIPForLog(request) + " - " + duoperson.getUsername() + " has OPT-IN DuoForce Grouper Group");
return "DuoEnroll2FAOptResult";
} else {
duoperson.setOptInStatus(false);
return "DuoEnroll2FAOptResult"; //May be Routing to an Error page instead!!!
}
} else {
return "redirect:/secure";
}
}
return "DuoEnroll2FAOptIn";
}
@RequestMapping(value = "/2FAoptout")
public String twoFactorOptOut(@RequestParam(value = "_action", required = false, defaultValue = "landed") final String action,
@ModelAttribute("DuoPerson") DuoPersonObj duoperson, HttpSession session, HttpServletRequest request) {
String result = null;
switch (action) {
case "landed":
duoperson.setOptInStatus(true);
break;
case "optout":
if (duoperson.isOptInStatus()) {
return "redirect:/secure";
} else {
result = duoUsrService.objActionById(duoperson.getChicagoID(), "RemoveUserFromDuoForce");
if (result.equals("Y")) {
duoperson.setOptInStatus(false);
logger.info("2FA Info - " + getIPForLog(request) + " - " + duoperson.getUsername() + " has OPT-OUT DuoForce Grouper Group");
return "DuoEnroll2FAOptResult";
} else {
duoperson.setOptInStatus(true);
return "DuoEnroll2FAOptResult"; //May be Routing to an Error page instead!!!
}
}
}
return "DuoEnroll2FAOptOut";
}
/**
* *********************************************************************
* Calling by DuoDeviceMgmt Controller, for Device Reactivation Purposes
* **********************************************************************
*/
@RequestMapping(value = "/deviceReactivation")
public String deviceReactivation(@ModelAttribute("DuoPerson") DuoPersonObj duoperson, HttpServletRequest request, Principal principal,
BindingResult result, HttpSession session, SessionStatus status, ModelMap model) throws UnsupportedEncodingException, JSONException, Exception {
logger.info("2FA Info - " + getIPForLog(request) + " - " + "REACTIVATION Process-Device Data:" + duoperson.getChoosenDevice() + ' ' + duoperson.getPhonenumber() + ' ' + duoperson.getDeviceOS());
return "DuoEnrollStep5";
}
/**
* ************************************************************************
* Main Method to Handle the actual Device ID Creation and Association
* between the Device and User
* *************************************************************************
*/
@RequestMapping(method = RequestMethod.POST, params = "enrollUserNPhone")
public String processEnroll(
HttpServletRequest request, @ModelAttribute("DuoPerson") DuoPersonObj duoperson,
// @RequestParam(value = "_unknownMobileVerified", required = false, defaultValue = "N") final String unknownMobileVerified,
BindingResult result, HttpSession session, SessionStatus status, ModelMap model) throws UnsupportedEncodingException, Exception {
String phoneId;
String tokenId;
String userId;
String qrCode;
/**
* *******************************************************************************************
* Enrollment Procedure, first thing first!
*
* Check Whether User is a registered DUO User and Register First-Time
* User into DUO Database
* ********************************************************************************************
*/
if (duoperson.getUser_id() == null) {
userId = duoUsrService.createObjByParam(duoperson.getUsername(), duoperson.getFullName(), duoperson.getEmail(), null, null);
duoperson.setUser_id(userId);
//Session Attribute "Duo User ID" - ADDED
session.setAttribute("duoUserId", userId);
logger.info("2FA Info - " + getIPForLog(request) + " - " + "Duo User Account created for: " + duoperson.getUsername() + "|DuoUserID:" + duoperson.getUser_id());
}
//Attempts to try to Catpure F5/Browser Refresh during Device Activation, Not letting Users jump out without Activation success
if (StringUtils.hasLength(duoperson.getPhone_id())) {
String activeStatus = duoPhoneService.getObjStatusById(duoperson.getPhone_id());
if (activeStatus.equals("false")) {
qrCode = duoPhoneService.objActionById(duoperson.getPhone_id(), "qrCode");
duoperson.setQRcode(qrCode);
model.put("deviceNotActive", true);
return "DuoActivationQR";
} else {
logger.info("2FA Info - " + getIPForLog(request) + " - " + duoperson.getUsername() + " Successfully Registered: PhoneNumber:" + duoperson.getPhonenumber() + "Tablet Name:" + duoperson.getTabletName());
return "DuoEnrollSuccess";
}
}
/**
* ***********************************************************************************
* Enrollment Procedure for Type == unknown
*
* Unknown == Old CellPhone that doesn't support Duo Mobile App, but
* still support SMS
* ***********************************************************************************
*/
if (duoperson.getChoosenDevice().matches("mobile") && duoperson.getDeviceOS().matches("unknown")) {
phoneId = duoPhoneService.createObjByParam(duoperson.getCompletePhonenumber(), duoperson.getChoosenDevice(), duoperson.getDeviceOS(), null, null);
duoperson.setPhone_id(phoneId);
duoPhoneService.associateObjs(duoperson.getUser_id(), phoneId);
}
/**
* ********************************************************************
* Enrollment Procedure for Type == Mobile | Tablet
*
* 1st) Create the Phone/Tablet Device first in DUO DB
*
* 2nd) Link the newly create Phone/Tablet to the user
*
* 3rd) Generate and Display the Activation QR code for DUO Mobile App
* Registration
* ********************************************************************
*/
if (duoperson.getChoosenDevice().matches("mobile|tablet") && !duoperson.getDeviceOS().matches("unknown")) {
phoneId = duoPhoneService.createObjByParam(duoperson.getCompletePhonenumber(), duoperson.getChoosenDevice(), duoperson.getDeviceOS(), duoperson.getTabletName(), null);
duoperson.setPhone_id(phoneId);
duoPhoneService.associateObjs(duoperson.getUser_id(), phoneId);
qrCode = duoPhoneService.objActionById(duoperson.getPhone_id(), "qrCode");
duoperson.setQRcode(qrCode);
return "DuoActivationQR";
}
/**
* ******************************************
* Enrollment Procedure for Type == LandLine
* ******************************************
*/
if (duoperson.getChoosenDevice().matches("landline")) {
phoneId = duoPhoneService.createObjByParam(duoperson.getCompletePhonenumber(), duoperson.getChoosenDevice(), null, null, duoperson.getLandLineExtension());
duoperson.setPhone_id(phoneId);
duoPhoneService.associateObjs(duoperson.getUser_id(), phoneId);
}
/**
* ****************************************************************
* Enrollment Procedure for Type == Token
*
* 1) Validate against the database to see whether Token has been
* register by somebody else || Token Existence in DB
*
* 2) Then ASSOCIATE the Token with the User
* ****************************************************************
*/
if (duoperson.getChoosenDevice().matches("token")) {
logger.debug("2FA Debug - " + getIPForLog(request) + "|" + duoperson.getUsername() + " is registering Token:" + duoperson.getTokenType() + "/" + duoperson.getTokenSerial());
deviceExistDuoValidator.validate(duoperson, result);
if (result.hasErrors()) {
return "DuoEnrollToken";
}
tokenId = duoTokenService.getObjByParam(duoperson.getTokenSerial(), duoperson.getTokenType(), "tokenId");
duoperson.setTokenId(tokenId);
duoTokenService.associateObjs(duoperson.getUser_id(), tokenId);
String readabletype = null;
switch (duoperson.getTokenType()) {
case "d1":
readabletype = "Duo-D100";
break;
case "yk":
readabletype = "YubiKey AES";
break;
case "h6":
readabletype = "HOTP-6";
break;
case "h8":
readabletype = "HOTP-8";
break;
case "t6":
readabletype = "TOTP-6";
break;
case "t8":
readabletype = "TOTP-8";
break;
}
duoperson.setTokenType(readabletype);
}
model.put("deviceActive", "Yes");
logger.info("2FA Info - " + getIPForLog(request) + " - " + duoperson.getUsername() + " Successfully Registered: PhoneNumber:" + duoperson.getPhonenumber()
+ " Tablet Name:" + duoperson.getTabletName() + " Token SN:" + duoperson.getTokenSerial());
return "DuoEnrollSuccess";
}
/**
* ****************************************
* Drop Down List for Token Type Selection
*
* Used in: DuoEnrollToken.jsp
*
*****************************************
*/
@ModelAttribute("tokenTypeList")
public Map<String, String> populateTokenTypeList() {
Map<String, String> tokenType = new LinkedHashMap<>();
tokenType.put("d1", "Duo-D100 hardware token");
tokenType.put("yk", "YubiKey AES hardware token");
tokenType.put("h6", "HOTP-6 hardware token");
tokenType.put("h8", "HOTP-8 hardware token");
tokenType.put("t6", "TOTP-6 hardware token");
tokenType.put("t8", "TOTP-8 hardware token");
return tokenType;
}
/**
* ****************************************
* Drop Down List for Tablet OS Selection
*
* Used in: DuoEnrollTablet.jsp
*
******************************************
*/
@ModelAttribute("tabletOSList")
public Map<String, String> populateTabletOSList() {
Map<String, String> tabletOS = new LinkedHashMap<>();
tabletOS.put("apple ios", "Apple IOS");
tabletOS.put("google android", "Google Android");
// tabletOS.put("windows phone", "Microsoft Windows for Surface");
return tabletOS;
}
/**
* *********************************************************************
* Below are ALL FOR creating the International Dial Code Drop Down list
*
* Used in: DuoEnrollStep3.jsp
* *********************************************************************
*/
@ModelAttribute("countryDialList")
public Map<String, String> populatecountryDialList() {
List<DuoEnrollController.Country> countries = new ArrayList<>();
Map<String, String> dialCodes = new LinkedHashMap<>();
Map<String, String> sortedDialCodes = new LinkedHashMap<>();
PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
//
// Get ISO countries, create Country object and
// store in the collection.
//
String[] isoCountries = Locale.getISOCountries();
for (String country : isoCountries) {
Locale locale = new Locale("en", country);
String code = locale.getCountry();
String name = locale.getDisplayCountry();
if (!"".equals(code) && !"".equals(name)) {
try {
int dialCode = phoneUtil.parse("1112223333", code).getCountryCode();
countries.add(new DuoEnrollController.Country(code, name, dialCode));
} catch (Exception e) {
}
}
}
Collections.sort(countries, new DuoEnrollController.CountryComparator());
for (DuoEnrollController.Country country : countries) {
dialCodes.put("+" + String.valueOf(country.dialCode), country.name);
//dialCodes.put("+"+String.valueOf(country.code), country.name);
}
sortedDialCodes = sortByValue(dialCodes);
return sortedDialCodes;
}
private static class Country {
private String code;
private String name;
private int dialCode;
Country(String code, String name, int dialCode) {
this.code = code;
this.name = name;
this.dialCode = dialCode;
}
}
static class CountryComparator implements Comparator {
private Comparator comparator;
CountryComparator() {
comparator = Collator.getInstance();
}
@SuppressWarnings("unchecked")
@Override
public int compare(Object o1, Object o2) {
DuoEnrollController.Country c1 = (DuoEnrollController.Country) o1;
DuoEnrollController.Country c2 = (DuoEnrollController.Country) o2;
return comparator.compare(c1.name, c2.name);
}
}
public static <K, V extends Comparable<? super V>> Map<K, V> sortByValue(Map<K, V> map) {
List<Map.Entry<K, V>> list =
new LinkedList<>(map.entrySet());
Collections.sort(list, new Comparator<Map.Entry<K, V>>() {
@Override
public int compare(Map.Entry<K, V> o1, Map.Entry<K, V> o2) {
return (o1.getValue()).compareTo(o2.getValue());
}
});
Map<K, V> result = new LinkedHashMap<>();
for (Map.Entry<K, V> entry : list) {
result.put(entry.getKey(), entry.getValue());
}
return result;
}
}