謎言語使いの徒然

適当に気になった技術や言語を流すブログ。

Bootstrap 以外の css フレームワーク

有名どころ。
個人的にきになったやつのみ

http://foundation.zurb.com/foundation.zurb.com

サイトや email に使用できるフレームワーク
CSS フレームワークというより、HTML の組み方なんかも固定となっている。
やたらと部品が豊富で、一種のラブラリ集と化している雰囲気。

semantic-ui.com

Semantic is a development framework that helps create beautiful, responsive layouts using human-friendly HTML. Human-Friendly な HTML で美しく、レスポンシブレイアウトの開発を支援する開発フレームワーク

サンプルを見てもわかりやすい。
学習コストが低いのはいい事です。

getmdl.io

Google の提唱する MaterialDesign を実装するコンパクトなフレームワーク
コンパクトなだけあって昨日は低めだけど、Material-UI よりはだいぶ安定してるかな?

www.material-ui.com

Goole 製 MaterialDesign フレームワーク
公式で React を謳うとか、Polymer や Angular はどーした?

Google は結構、OSS でさえあれば、自社製に拘らない所があるし、ありえるっちゃありえるか…

v1.0 からマジで React 前提になってる臭い。

https://material-ui-1dab0.firebaseapp.com/getting-started/usage

この辺も参考になるかも?

現時点で Reactを Gulp 上コンパイルして見る

教科書はこれ

qiita.com

package.json をしれっと。

{
  "name": "react-base",
  "version": "0.1.0",
  "description": "React base application template",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "BSD-2-Clause",
  "dependencies": {
    "gulp-util": "^3.0.8",
    "react": "^15.6.1",
    "react-dom": "^15.6.1"
  },
  "devDependencies": {
    "autoprefixer": "^7.1.2",
    "babel": "^6.23.0",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-react": "^6.24.1",
    "babelify": "^7.3.0",
    "browserify": "^14.4.0",
    "cssnano": "^3.10.0",
    "gulp": "^3.9.1",
    "gulp-postcss": "^7.0.0",
    "gulp-sass": "^3.1.0",
    "gulp-sourcemaps": "^2.6.0",
    "gulp-uglify": "^3.0.0",
    "gulp-webserver": "^0.9.1",
    "rimraf": "^2.6.1",
    "vinyl-buffer": "^1.0.0",
    "vinyl-source-stream": "^1.1.0"
  },
  "scripts": {
    "start": "gulp",
    "clean": "gulp clean",
    "compile": "gulp compile"
  }
}

とりあえず各バージョンは上記の通り。
その上で gulpfile.js を下記のようにすればまずコンパイルできる

var gulp = require('gulp');
var gutil = require('gulp-util');

var browserify = require('browserify');
var babelify = require('babelify');

var source = require('vinyl-source-stream');
var buffer = require('vinyl-buffer');
var sourcemaps = require('gulp-sourcemaps');

var uglify = require('gulp-uglify');

var webserver = require('gulp-webserver');

let SOURCE_DIR = './src';
let JS_SOURCE_DIR = SOURCE_DIR + '/main';

let DIST_DIR = './dist';

// React コードのコンパイル設定
let jsCompileTask = browserify({
  entries: [ JS_SOURCE_DIR + '/index.js'],
  transform: ['babelify'],
  cache: {},
  packageCache: {}
});

// コンパイル実行箇所
function compileJs() {
  function createErrorHandler(name) {
    return function (err) {
      console.error('Error from ' + name + ' in compress task', err.toString());
    };
  }

  return jsCompileTask.bundle() // ソースをコンパイル
    .on('error', gutil.log.bind(gutil, 'Browserify Error')) // コンパイルエラー出力
    .pipe(source('main.min.js')) // 全てのソースをまとめた JavaScript 出力名
    .pipe(buffer())  // バッファリング
    .pipe(sourcemaps.init({loadMaps: true})) // 既に出てるソースマップ取り込み
    .pipe(uglify())  // コード圧縮
    .on('error', createErrorHandler('uglify'))  // エラーメッセージ
    .pipe(sourcemaps.write('./'))  // デバッグ情報出力
    .pipe(gulp.dest(DIST_DIR + '/js'));  // 出力ディレクトリ
}

function runServer() {
  gulp.src(DIST_DIR)
    .pipe(webserver({
      livereload: true,
      open: true,
      fallback: '/index.html'
    }));  
}

function watch() {
  gulp.watch(JS_SOURCE_DIR + '/**/*.js', ['compileJs']);
}

gulp.task('compileJs', compileJs);

gulp.task('compile', ['compileJs'])

gulp.task('watch', watch);
gulp.task('debug', ['compile', 'watch'], runServer);

gulp.task('default', ['debug']);

後は scss でもなんでもすればおk

ただし、この構成は欠点があって、CSS ファイルを include すると死ぬ。
多分 babel-preset-react の問題だと思う。

Gradle4 でマルチプロジェクト

教科書はこれ。 第57章 マルチプロジェクトのビルド

まずは Jersey の設定を作って、分離するものを指定する。

Jerseyの設定2(web.xmlとかApplicationクラスとか) - edgegram

サーブレット設定をソースで

web.xml ファイルは昔は役に立ったアーキテクチャだ。
一つの Tomcat で複数のサーブレットをデプロイするにあたって、そのデプロイ設定を行うにはリーズナブルだったろう。

だが、はっきり言おう、今日日1サーバにそう何個もデプロイなんざしねーよと(汗
MVC ができてルーティングができるようになった時から、その役目を失ったと言える。

ということで設定

package net.white_azalea

import org.glassfish.jersey.server.ResourceConfig
import org.glassfish.jersey.server.ServerProperties
import javax.ws.rs.ApplicationPath

@ApplicationPath("/")
class ApplicationSetting : ResourceConfig {

    constructor() {
        packages("net.white_azalea")
        property(ServerProperties.PROVIDER_SCANNING_RECURSIVE, true)
    }
}

そしてこれがあると、設定ファイル( web.xml )いらずなのです。

そしてプロジェクト分離

設定一つで、管理しようと思う。
何って完全分離とか難しすぎた

目的はこんな感じに配置すること。

  • ROOT
    • build.gradle
    • settings.gradle
    • src/main/kotlin/net/white_azalea/ApplicationSetting.kt
      プロジェクト設定を含むソース
    • settings
      サブプロジェクト
      • src/main/kotlin/net/white_azalea/api/Example.kt ソース

まずは ROOT/settings.gradle

include 'settings'

次に、ROOT/build.gradle を編集

apply plugin: 'maven'
apply plugin: 'war'
apply plugin: 'org.akhikhl.gretty'
apply plugin: 'kotlin'

sourceCompatibility = 1.8
def jerseyVersion = '2.25.1'

buildscript {
  repositories {
    jcenter()
    mavenCentral()
  }

  dependencies {
    classpath 'org.akhikhl.gretty:gretty:+'
    classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.1.3'
  }
}

def thisGroup = 'net.white-azalea'
def thisVersion = '1.0'

allprojects {
    apply plugin: 'maven'
    apply plugin: 'war'

    group = thisGroup
    version = thisVersion

    repositories {
        jcenter()
        mavenCentral()
        mavenLocal()
    }

    dependencies {
        compile 'javax.ws.rs:javax.ws.rs-api:2.0.1'
        compile "org.glassfish.jersey.containers:jersey-container-servlet:${jerseyVersion}"
        testCompile 'junit:junit:4.+'
    }
}

ext {
    appName = 'example'
    project.version = thisVersion
    project.group = thisGroup
    project.description = 'example gradle application.'
}

dependencies {
    compile project(':settings')
}

ここまで書いたら後は settings ディレクトリを掘ってファイルを配置すればおk

Gradle4 + Jersey2 + Kotlin でRESTサービスを作ってみる

SPA でアプリ作ろう思って、サーバに当初 Spring 考えてたけど、こっちの方が API サーバが楽そうだった。

で、Kotlin は個人的な好み

教科書はこれ

JAX-RS(Jersey)+GradleでWebアプリを作る - Olivinecafe - blog

Gradle

汎用ビルドツール。
Java 専用ではないので、Java 書きたければ基本はプラグイン
独自の DSL で記述するが、Java ライブラリとか普通に叩けた。

個人的に好きなのは、gradle wrapper コマンドで、これを実行するとカレントディレクトリに gradlew コマンドを吐き出す。
こいつは単独実行可能な gradle コマンドなので、いちいち使う人に Gradle のインストールを強要しない。

sbt も大概 jar 一つでやってのけるのだが、こういう仕様がないと、複数人での開発がだるくて仕方ない。

とりあえず、build.gradle はこんな感じで書いた。

apply plugin: 'maven'
apply plugin: 'war'
apply plugin: 'org.akhikhl.gretty'
apply plugin: 'kotlin'

def jerseyVersion = '2.25.1'

sourceCompatibility = 1.7

buildscript {
  repositories {
    jcenter()
    mavenCentral()
  }

  dependencies {
    classpath 'org.akhikhl.gretty:gretty:+'
    classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.1.3'
  }
}

ext {
    project.version = '0.0.1'
    project.group = 'net.white-azalea'
    appName = 'example'
    project.description = 'example gradle application/'
}

configurations {
     all*.exclude module: 'servlet-api'
}

dependencies {
    providedCompile 'javax.ws.rs:javax.ws.rs-api:2.0.1'
    compile "org.glassfish.jersey.containers:jersey-container-servlet:${jerseyVersion}"
    testCompile 'junit:junit:4.+'
}

repositories {
    jcenter()
    mavenCentral()
    mavenLocal()
}

とてもざっくり説明すると、

  • リリース時は war でビルドするよ
  • gretty プラグインを使って、デバック実行時に jetty サーバ使うよ
  • Kotlin 言語でやるよ
    しかも Java プラグイン入れてないので、Java 書けないよ
  • servlet-api は war には含まない様にしてるよ
    これが含まれてると、Tomcat とかのサーバにデプロイした時バージョン衝突とかめんどくさいことになる場合がある
  • Jersey2.25.1 を必要とするよ

これを gradlew appRun すると、依存する jar とか、実行サーバとか諸々勝手にダウンロードしてくる。
開発マシンにわざわざ手動で Tomcat 入れさせる環境とか、ネットワーク接続禁止環境だけでしょ…Web アプリ作るのにネットワーク禁止とかこれいかに?

jersey2

JAX-RS 実装らしいがなんのことはない。
アノテーションでパス指定すれば色々できる。

まずは、src/main/webapp/WEB-INF/web.xml を記述する。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    version="2.5">
    <servlet>
        <servlet-name>jersey-example</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
            <param-name>jersey.config.server.provider.packages</param-name>
            <param-value>net.white_azalea</param-value>
        </init-param>
        <init-param>
            <param-name>jersey.config.server.provider.scanning.recursive</param-name>
            <param-value>true</param-value>
        </init-param>
        <load-on-startup>10</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>jersey-example</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>

設定は全て Gradle にお任せモードする web.xml だ。

お次はソース。src/main/kotlin/net.white-azalea/Example.kt を作成して以下を記述

package net.white_azalea

import javax.ws.rs.GET
import javax.ws.rs.Path
import javax.ws.rs.Produces
import javax.ws.rs.core.MediaType

@Path("/hello")
class Example {

    @GET
    @Produces(MediaType.TEXT_HTML)
    fun hello(): String = "Hello!"
}

ここまできたら全部おしまい。

./gradlew appRun して、http://localhost:8080/jersey-example/hello にアクセスできれば OK.

jar 依存がだいぶ足りてないので、オブジェクト返すとコケる。
まぁその辺は この辺 みて自力解決しよう。

Redmine3 に Backlogs を入れるメモ

先日入れた Redmine 3.3 にBacklogs を突っ込んでみる。
教科書はこれ。

interprism.hatenablog.com

確かに Gem 競合を除いてしまえば入るっちゃ入る。

[root@localhost redmine]# RAILS_ENV=production bundle exec rake redmine:backlogs:install
3.3.4.stable.16875. You are running backlogs v1.0.6, latest version is 1.0.6

=====================================================
             Redmine Backlogs Installer
=====================================================
Installing to the production environment.
Fetching card labels from http://git.gnome.org...done!
Configuring story and task trackers...
-----------------------------------------------------
Which trackers do you want to use for your stories?
  1. ストーリー
  2. バグ
  3. 機能
  4. サポート
Separate values with a space (e.g. 1 3): 1
You selected the following trackers: ストーリー. Is this correct? (y/n) y
-----------------------------------------------------
Which tracker do you want to use for your tasks?
  1. バグ
  2. 機能
  3. サポート
Choose one from above (or choose none to create a new tracker): 3
You selected サポート. Is this correct? (y/n) y
Story and task trackers are now set.
Migrating the database...WARNING:  進行中のトランザクションがありません
done!
Installation complete. Please restart Redmine.
Thank you for trying out Redmine Backlogs!
[root@localhost redmine]# 

CentOS7 に Redmine 3 を試してみるテスト

とりま VirtualBox に CentOS7 を普通にインストールして、ホストオンリーアダプタを有効化。

# nmcli connection modify enp0s3 connection.autoconnect yes

これで普通に ssh 可能となるので、ここからはインストール開始。

あとは下記の手順で Redmine インストール。

Redmine 3.3をCentOS 7.3にインストールする手順 | Redmine.JP Blog

因みに下記のエラーを食った。

-----
mysql client is missing. You may need to 'apt-get install libmysqlclient-dev' or 'yum install mysql-devel', and try again.
-----
*** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.

Provided configuration options:
    --with-opt-dir
    --without-opt-dir
    --with-opt-include=${opt-dir}/include
    --with-opt-lib=${opt-dir}/lib
    --with-make-prog
    --without-make-prog
    --srcdir=.
    --curdir
    --ruby=/usr/local/bin/$(RUBY_BASE_NAME)
    --with-mysql-dir
    --without-mysql-dir
    --with-mysql-include
    --without-mysql-include=${mysql-dir}/include
    --with-mysql-lib
    --without-mysql-lib=${mysql-dir}/lib
    --with-mysql-config
    --without-mysql-config
    --with-mysql-dir
    --without-mysql-dir
    --with-mysql-include
    --without-mysql-include=${mysql-dir}/include
    --with-mysql-lib
    --without-mysql-lib=${mysql-dir}/lib
    --with-mysqlclientlib
    --without-mysqlclientlib

To see why this extension failed to compile, please check the mkmf.log which can be found here:

  /var/lib/redmine/vendor/bundle/ruby/2.3.0/extensions/x86_64-linux/2.3.0-static/mysql2-0.4.8/mkmf.log

extconf failed, exit code 1

Gem files will remain installed in /var/lib/redmine/vendor/bundle/ruby/2.3.0/gems/mysql2-0.4.8 for inspection.
Results logged to /var/lib/redmine/vendor/bundle/ruby/2.3.0/extensions/x86_64-linux/2.3.0-static/mysql2-0.4.8/gem_make.out

An error occurred while installing mysql2 (0.4.8), and Bundler cannot continue.
Make sure that `gem install mysql2 -v '0.4.8'` succeeds before bundling.

In Gemfile:
  mysql2

見たまま、mysql client のライブラリをインストールしたら治った。

だがその後も下記に引っかかった。

show-surumegohan.blogspot.jp

いったいいくつ引っかかるんだ…

そして、Apache を起動するも、デフォルトページが表示。

/etc/httpd/conf.d/welcome.conf をリネームで退避するも、403 Forbidden.

ログを眺めると

[Mon Jul 24 22:42:58.583973 2017] [autoindex:error] [pid 25600] [client 192.168.56.1:50949] AH01276: Cannot serve directory /var/lib/redmine/public/: No matching DirectoryIndex (index.html) found, and server-generated directory index forbidden by Options directive

だー!

[Redmine Rails] Passenger がシンボリックリンクを解決してくれない | Javable.Jp とか 色々見たけど、なんのことはない、Passenger のインストールでエラー出てただけだった。
やり直したら入る不思議…爆発しる!

まぁとりあえずこれでインストールはできた。
毎度思うがハマりどころが多すぎませんかね?(汗

SPA アプリそろそろ作って見たいんじゃい(3)

とりあえず SpringBootSecurity を設定してみた回

まずは build.gradle に依存を追加する。

dependencirs{
    compile('org.springframework.boot:spring-boot-starter-security')
    // あとはおすきに

そしてログインに使用する DB からデータを取得するためのインターフェースを追加する。
基本的に DB にアカウントとパスワードを格納しているので、それを取得する。

interface AccountRepository : JpaRepository<Account, Long> {
    fun findOneByName(name: String): Optional<Account>
}

次に、Spring がログインの際に使用するユーザ情報のデータ定義を作る。

package net.white_azalea.todo_demo.service.secure

import net.white_azalea.todo_demo.repositories.Account
import org.springframework.security.core.authority.AuthorityUtils
import org.springframework.security.core.userdetails.User

class LoginAccount(account: Account) : User(account.name, account.password, AuthorityUtils.createAuthorityList("LOGIN_ROLE")) {

}

中身はあってもなくてもいい。
要するにアカウント ID と、ハッシュ化されたパスワードがあり、その時の権限を用意する。

次は Spring がアカウント ID を受け付けたときにログイン情報を検索するロジックの実装。

package net.white_azalea.todo_demo.service.secure

import net.white_azalea.todo_demo.repositories.AccountRepository
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.stereotype.Component

@Component
class AccountDetailService(
        val accountRepository: AccountRepository
) : UserDetailsService {
    override fun loadUserByUsername(username: String?): UserDetails {
        val acc = accountRepository.findOneByName(username.orEmpty())

        if (acc.isPresent) {
            return LoginAccount(acc.get())
        } else {
            throw UsernameNotFoundException("Account does not exists.")
        }
    }
}

パスワードのエンクリプトと、チェックを行うためのクラスも作成。

package net.white_azalea.todo_demo.service.secure

import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Component
import java.util.*
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

@Component
class Crypto : PasswordEncoder {

    override fun encode(rawPassword: CharSequence?): String {
        if (rawPassword == null) {
            return ""
        }
        return this.sign("SecretKey", rawPassword.toString())
    }

    /**
     * Check password matching.
     */
    override fun matches(rawPassword: CharSequence?, encodedPassword: String?): Boolean {
        if (rawPassword == null || encodedPassword == null) {
            return false
        }
        return this.sign("SecretKey", rawPassword.toString()).equals(encodedPassword)
    }

    /**
     * Sign with key and value.
     *
     * Not good to use password hash.
     * TODO: Have to use bcrypt to password.
     * https://docs.spring.io/spring-security/site/docs/current/apidocs/org/springframework/security/crypto/bcrypt/BCrypt.html
     */
    fun sign(key: String, value: String): String {
        val secretKey = SecretKeySpec(key.toByteArray(), "HmacSHA1")
        val mac = Mac.getInstance("HmacSHA1")
        mac.init(secretKey)
        val bytes = mac.doFinal(value.toByteArray())
        return Base64.getEncoder().encodeToString(bytes)
    }
}

そしてこれらを使ってログインをするための設定クラス

package net.white_azalea.todo_demo.service.secure

import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.config.annotation.authentication.configurers.GlobalAuthenticationConfigurerAdapter
import org.springframework.security.core.userdetails.UserDetailsService

@Configuration
class AuthenticationConfiguration(
        val userDetailsService: UserDetailsService,
        val crypto: Crypto
) : GlobalAuthenticationConfigurerAdapter() {

    override fun init(auth: AuthenticationManagerBuilder?) {
        auth?.userDetailsService(userDetailsService)
                ?.passwordEncoder(crypto)
    }
}

最後に設定これがあれば /loginaccount, password を POST すれば認証処理が走る。

package net.white_azalea.todo_demo

import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.builders.WebSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.web.authentication.AuthenticationFailureHandler
import org.springframework.security.web.util.matcher.AntPathRequestMatcher

@Configuration
@EnableWebSecurity
class SecurityConfig : WebSecurityConfigurerAdapter(false) {

    override fun configure(web: WebSecurity?) {
        web?.ignoring()?.antMatchers(
            "/images/**",
            "/css/**",
            "/javascript/**",
            "/webjars/**")
    }

    fun authFailure(): AuthenticationFailureHandler {
        return AuthenticationFailureHandler { request, response, exception ->
            response.status = 200
            response.sendRedirect("/?error=Failed to access.")
        }
    }

    override fun configure(http: HttpSecurity?) {

        // 認可設定
        http?.authorizeRequests()
                ?.antMatchers("/", "/login")?.permitAll() // index は全ユーザアクセス許可
                ?.anyRequest()?.authenticated() // それ以外は認証要求

        // ログイン認定
        http?.formLogin()
                ?.loginProcessingUrl("/login") // login url
                ?.loginPage("/") // ログインフォームのパス
                ?.failureHandler(authFailure()) // ログイン失敗時のハンドラ
                ?.defaultSuccessUrl("/menu") // ログイン結果
                ?.usernameParameter("account") // ユーザ名パラメータ
                ?.passwordParameter("password")  // パスワードのパラメータ名
                ?.and()

        // ログアウト設定
        http?.logout()
                ?.logoutRequestMatcher(AntPathRequestMatcher("/logout**")) // ログアウト処理のパス
                ?.logoutSuccessUrl("/") // ログアウト完了時のパス
    }
}

実際試して見たが、Cookie 嬢の JSESSION_ID なんかもきちんと更新されていたし、セッションハイジャック対策もそこそこされているのだろう。
送信フォームもこんなカンジ

<!DOCTYPE HTML>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <title>top page</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
    <h1>Login form!</h1>
    <form action="/login" method="post" th:action="@{'/login'}">
        <div>Account:</div>
        <input type="text" name="account" />
        <div>Pass:</div>
        <input type="password" name="password" />
        <button type="submit">Do login</button>
    </form>
</body>
</html>

気をつけなければならないのは @{'/login'} の部分。
これで作成しないと、URL に CSRF ヘッダも生成されない。つまり POST が蹴られる。

やっぱ大分考慮されてるのねーってカンジである。