Skip to content

Instantly share code, notes, and snippets.

@tyru
Last active February 26, 2016 13:29
Show Gist options
  • Save tyru/b0d908997003cad43331 to your computer and use it in GitHub Desktop.
Save tyru/b0d908997003cad43331 to your computer and use it in GitHub Desktop.
Spring Boot + AngularJS
package com.example.controller.api;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@RestController
@RequestMapping("/api")
public class ApiController {
private static final String PROP_FILE = "mail.properties";
private Map<String, String> config;
private static final String PROP_MAIL_FROM_ADDRESS = "mail.from-address";
private static final String PROP_MAIL_DEBUG = "mail.debug";
private static final String PROP_SMTP_HOST = "mail.smtp-host";
private static final String PROP_MAIL_TITLE = "mail.title";
private static final Logger logger = LoggerFactory.getLogger(ApiController.class);
@Autowired
private AddressRepository addressRepos;
/**
* Load config.
*/
public ApiController() {
logger.info("Constructing ApiController...");
Properties prop = new Properties();
final InputStream in = ApiController.class.getClassLoader().getResourceAsStream(PROP_FILE);
if (in == null) {
throw new IllegalArgumentException(PROP_FILE + " not found.");
}
try {
prop.load(in);
config = new HashMap<>();
for (final String key : prop.stringPropertyNames()) {
final String value = prop.getProperty(key);
config.put(key, value);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
@RequestMapping(method = RequestMethod.GET, value = "/address")
public List<Address> getAddressList() {
logger.info("GET /address");
return addressRepos.findAll();
}
@RequestMapping(method = RequestMethod.GET, value = "/address/{id:^(?:[1-9]|[1-9][0-9]*)$}")
public Address getAddressById(@PathVariable long id) {
logger.info("GET /address/" + id);
final Address address = addressRepos.findAddressById(id);
if (address != null) {
return address;
} else {
throw new AddressNotFound(id);
}
}
@RequestMapping(method = RequestMethod.POST, value = "/mail")
public boolean postMail(@RequestBody Mail mail) {
logger.info("POST /mail");
logger.debug(mail.toString());
Properties props = new Properties();
props.put("mail.smtp.host", config.get(PROP_SMTP_HOST));
final boolean mailDebug = Boolean.valueOf(config.get(PROP_MAIL_DEBUG));
if (mailDebug) {
props.put("mail.debug", "true");
}
Session session = Session.getInstance(props, null);
session.setDebug(mailDebug);
try {
Message msg = new MimeMessage(session);
final String fromAddress = config.get(PROP_MAIL_FROM_ADDRESS);
msg.setFrom(new InternetAddress(fromAddress));
InternetAddress[] address = new InternetAddress[mail.getIds().size()];
for (int i = 0; i < mail.getIds().size(); i++) {
String addr = getAddressById(mail.getIds().get(i)).getAddress();
address[i] = new InternetAddress(addr);
}
msg.setRecipients(Message.RecipientType.TO, address);
msg.setSubject(config.get(PROP_MAIL_TITLE));
msg.setSentDate(new Date());
msg.setText(mail.getBody());
Transport.send(msg);
} catch (MessagingException e) {
e.printStackTrace();
}
return true;
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class Mail {
private List<Long> ids;
private String body;
}
package com.example;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import com.example.controller.api.Address;
import com.example.controller.api.AddressRepository;
@SpringBootApplication
public class Application {
private static final Logger logger = LoggerFactory.getLogger(Application.class);
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public CommandLineRunner demo(AddressRepository repository) {
return (args) -> {
logger.info("Saving demo data...");
repository.save(new Address("拓也", "tyru.exe+takuya@gmail.com"));
repository.save(new Address("ほげら", "tyru.exe+hogera@gmail.com"));
repository.save(new Address("もげら", "tyru.exe+mogera@gmail.com"));
repository.save(new Address("はなもげら", "tyru.exe+hanamogera@gmail.com"));
logger.info("Saving demo data...Done.");
};
}
}
mail.from-address=tyru.exe+angular.demo@gmail.com
mail.debug=true
mail.smtp-host=smtp
mail.title=こんにちは
<!DOCTYPE html>
<html ng-app="mailApp">
<head ng-controller="htmlHeaderCtrl">
<meta charset="utf-8" />
<title>Getting Started Angular</title>
<script type="text/javascript" src="/js/ext/angular.min.js"></script>
<script type="text/javascript" src="/js/ext/angular-route.min.js"></script>
<script type="text/javascript" src="/js/ext/underscore-min.js"></script>
<script type="text/javascript" src="/js/mail.js"></script>
<link rel="stylesheet" href="/css/mail/common.css" />
<link rel="stylesheet" ng-href="/css/mail/{{css}}"
ng-repeat="css in shared.optCssList" />
<base href="/mail/">
</head>
<body>
<!--
大まかすぎる画面遷移の流れ
・メールを書く
・あて先を書く
・メール送信
ばあちゃんでもわかるようになるべく手紙のレイアウトに似せる
-->
<!-- ヘッダー:画面遷移させるボタン等を配置 -->
<div id="header" ng-controller="headerCtrl">
<!-- XXX:
「次へ」ボタンが手元のリモコンではなく
Web画面上に表示されている意味はあるのか?
(現在入力用デバイスに何を使うかは検討中) -->
<!-- TODO:
メールを書く時など通常操作時はヘッダーはなるべく表示しない。
何か専用のボタンが押された時のみ表示する。 -->
<span id="next-btn" ng-class="{disable: shared.isDisabledNextURL()}" class="item">
<a href="{{shared.nextURL}}" ng-click="handleNextButtonClick()">次に進む</a>
</span>
<h1 id="title">{{shared.title}}</h1>
<!-- TODO --><span id="help-btn" class="item">困ったときは</span>
</div>
<!-- メインの表示領域:URLによってビューを切り替える -->
<div id="container" ng-view></div>
<script type="text/ng-template" id="mailEdit.html">
<!-- hidden element -->
<textarea id="textarea-body" ng-model="shared.mailBody" ng-blur="focusTextArea()"></textarea>
<ul id="body">
<!-- TODO: 適切な字数で改行(ラップアラウンド)する -->
<li class="line" ng-repeat="line in lines"><span ng-if="line.text">{{line.text}}</span><span ng-if="!line.text"> </span></li>
</ul>
</script>
<script type="text/ng-template" id="addrEdit.html">
<p ng-if="!addrList">連絡先を読み込んでいます...</p>
<div ng-attr-id="address-{{addr.id}}" ng-class="addr.selected ? '' : 'disable'" class="address" ng-repeat="addr in addrList">
<a ng-click="addrList[$index].selected = ! addrList[$index].selected">{{addr.name}}</a>
</div>
</script>
<script type="text/ng-template" id="mailSentCtrl.html">
</script>
</body>
</html>
(function() {
"use strict";
var app = angular.module("mailApp", ['ngRoute']);
// FIXME: Cursor string is placed same as mail text, but underline.
var ENABLE_INSERT_CURSOR = false;
var CURSOR_STRING = '_';
var DISABLED_NEXT_URL = '#';
// Router, and so on
app.config(['$locationProvider', '$routeProvider', function($locationProvider,$routeProvider) {
$locationProvider.html5Mode(true);
$routeProvider.when('/mailEdit', {
templateUrl: 'mailEdit.html',
controller: 'mailEditCtrl'
})
$routeProvider.when('/addrEdit', {
templateUrl: 'addrEdit.html',
controller: 'addrEditCtrl'
})
$routeProvider.otherwise({
redirectTo: '/mailEdit'
});
}]);
// Shared structure between controllers
app.factory("SharedService", function() {
return {
set: function(items) {
if (this === window) {
throw new 'Internal Error: call set() like this: $scope.shared.set(...)';
}
// Set default
this.title = '';
this.nextURL = DISABLED_NEXT_URL;
this.optCssList = [];
this.handleNextButtonClick = function() {};
// FIXME: ページを戻った時に備えてクリアはしないでおきたいが、これだけクリアしないのは何か気持ち悪い
//this.mailBody = '';
// Set given values
for (var key in items) {
this[key] = items[key];
}
},
isDisabledNextURL: function() {
return this.nextURL === DISABLED_NEXT_URL;
},
title: '',
nextURL: DISABLED_NEXT_URL,
optCssList: [],
handleNextButtonClick: function() { return true; },
mailBody: ''
};
});
// For <head>, additional css list
app.controller('htmlHeaderCtrl', function ($scope, SharedService) {
$scope.shared = SharedService;
});
// Header, title, and so on
app.controller('headerCtrl', function ($scope, SharedService) {
$scope.shared = SharedService;
$scope.handleNextButtonClick = function() {
return $scope.shared.handleNextButtonClick();
};
});
// Mail edit page
app.controller('mailEditCtrl', function ($scope, SharedService) {
$scope.shared = SharedService;
$scope.shared.set({
title: '文章を書きます。',
optCssList: ['mailEditCtrl.css'],
nextURL: 'addrEdit'
});
$scope.$watch('shared.mailBody', function () {
// FIXME: This callback is not called when Enter is pressed.
var bodyText = $scope.shared.mailBody;
// Insert cursor string at cursorPos.
if (ENABLE_INSERT_CURSOR) {
var cursorPos = document.getElementById('textarea-body').selectionStart;
bodyText = bodyText.substr(0, cursorPos) + CURSOR_STRING + bodyText.substr(cursorPos);
}
$scope.lines = bodyText.split('\n').map(function(line) {
return {text: line};
});
})
$scope.focusTextArea = function() {
document.getElementById('textarea-body').focus();
};
$scope.focusTextArea();
});
// Address edit page
app.controller('addrEditCtrl', function ($scope, SharedService, $http) {
var ENABLED_NEXT_URL = 'mailSentCtrl';
$scope.shared = SharedService;
$scope.shared.set({
title: '誰に送りますか?',
optCssList: ['addrEditCtrl.css'],
nextURL: DISABLED_NEXT_URL,
handleNextButtonClick: function() {
if ($scope.shared.nextURL === DISABLED_NEXT_URL) {
alert("送る相手を選んでください。");
return false;
}
var data = {
ids: _.pluck(_.select($scope.addrList, function(address) {
return address.selected;
}), 'id'),
body: $scope.shared.mailBody
};
$http.post('/api/mail', data).success(function(result) {
console.dir(result);
});
return true;
}
});
$scope.addrList = [];
// Fetch address list data from backend server.
$http.get('/api/address').success(function(data) {
$scope.addrList = _.map(data, function(val) {
val.selected = false;
return val;
});
$scope.$watch('addrList', function(newValue, oldValue) {
function isSelected(val) {
return val.selected;
}
// Also apply to next button.
if (_.any(newValue, isSelected)) {
$scope.shared.nextURL = ENABLED_NEXT_URL;
} else {
$scope.shared.nextURL = DISABLED_NEXT_URL;
}
}, true);
});
});
// Mail sent page
app.controller('mailSentCtrl', function ($scope, SharedService) {
$scope.shared = SharedService;
$scope.shared.set({
title: 'メールを投函しました。',
optCssList: [],
nextURL: DISABLED_NEXT_URL
});
});
})();
package com.example.controller.mail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/mail")
public class MailController {
private static final Logger logger = LoggerFactory.getLogger(MailController.class);
@RequestMapping("/")
public String exampleIndex() {
logger.info("GET /mail");
return "mail";
}
@RequestMapping("/**")
public String exampleOther() {
logger.info("GET /mail/**");
// NOTE: Redirects to '/mail' even when a user reloads a page
// which is any page under /mail.
return "redirect:/mail/";
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment